@aleonnet/healthcare-scheduler 0.1.9 → 0.1.11
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/README.md +0 -1
- package/dist/database/repositories/EventsRepository.d.ts.map +1 -1
- package/dist/database/repositories/EventsRepository.js +4 -2
- package/dist/database/repositories/EventsRepository.js.map +1 -1
- package/dist/database/repositories/ItemsRepository.d.ts.map +1 -1
- package/dist/database/repositories/ItemsRepository.js +3 -1
- package/dist/database/repositories/ItemsRepository.js.map +1 -1
- package/dist/database/repositories/MedicationsRepository.d.ts.map +1 -1
- package/dist/database/repositories/MedicationsRepository.js +74 -60
- package/dist/database/repositories/MedicationsRepository.js.map +1 -1
- package/dist/database/repositories/OccurrencesRepository.d.ts.map +1 -1
- package/dist/database/repositories/OccurrencesRepository.js +3 -1
- package/dist/database/repositories/OccurrencesRepository.js.map +1 -1
- package/dist/database/repositories/PlansRepository.d.ts.map +1 -1
- package/dist/database/repositories/PlansRepository.js +9 -5
- package/dist/database/repositories/PlansRepository.js.map +1 -1
- package/dist/plugins/SyncPluginEventDriven.js +1 -1
- package/dist/plugins/SyncPluginEventDriven.js.map +1 -1
- package/dist/plugins/gamification/GamificationPlugin.d.ts.map +1 -1
- package/dist/plugins/gamification/GamificationPlugin.js +2 -9
- package/dist/plugins/gamification/GamificationPlugin.js.map +1 -1
- package/dist/reconciler/WindowScheduler.d.ts +2 -0
- package/dist/reconciler/WindowScheduler.d.ts.map +1 -1
- package/dist/reconciler/WindowScheduler.js +271 -262
- package/dist/reconciler/WindowScheduler.js.map +1 -1
- package/package.json +1 -1
|
@@ -5,6 +5,10 @@ const expandPlan_1 = require("../planning/expandPlan");
|
|
|
5
5
|
const windowPlanner_1 = require("../planning/windowPlanner");
|
|
6
6
|
const timestamp_1 = require("../utils/timestamp");
|
|
7
7
|
class WindowScheduler {
|
|
8
|
+
runSerial(task) {
|
|
9
|
+
WindowScheduler.opQueue = WindowScheduler.opQueue.then(task, task);
|
|
10
|
+
return WindowScheduler.opQueue;
|
|
11
|
+
}
|
|
8
12
|
constructor(plansRepo, occurrencesRepo, notificationDriver, eventBus) {
|
|
9
13
|
this.plansRepo = plansRepo;
|
|
10
14
|
this.occurrencesRepo = occurrencesRepo;
|
|
@@ -31,224 +35,237 @@ class WindowScheduler {
|
|
|
31
35
|
return undefined;
|
|
32
36
|
}
|
|
33
37
|
async fillWindow(options = {}) {
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
console.log(`📅 Found ${plans.length} active plans`);
|
|
44
|
-
for (const plan of plans) {
|
|
45
|
-
await this.occurrencesRepo.deleteByPlanIdAfter(plan.id, now.toISOString());
|
|
46
|
-
}
|
|
47
|
-
const allOccurrences = [];
|
|
48
|
-
for (const plan of plans) {
|
|
49
|
-
try {
|
|
50
|
-
const expanded = (0, expandPlan_1.expandPlan)(plan.schedule_rule, now);
|
|
51
|
-
const windowed = (0, windowPlanner_1.selectWindow)(expanded, { windowDays: days, maxPending: 1000 });
|
|
52
|
-
const dbOccurrences = windowed.map(occ => ({
|
|
53
|
-
id: `${plan.id}:${occ.scheduledAt}`,
|
|
54
|
-
plan_id: plan.id,
|
|
55
|
-
scheduled_at: occ.scheduledAt,
|
|
56
|
-
kind: (plan.item_type || 'MED'),
|
|
57
|
-
qty: occ.qty || 1
|
|
58
|
-
}));
|
|
59
|
-
allOccurrences.push(...dbOccurrences);
|
|
60
|
-
console.log(`📅 Plan ${plan.id}: ${dbOccurrences.length} occurrences`);
|
|
38
|
+
return await this.runSerial(async () => {
|
|
39
|
+
const { days = 14 } = options;
|
|
40
|
+
try {
|
|
41
|
+
console.log(`📅 Filling ${days}-day window...`);
|
|
42
|
+
const now = new Date();
|
|
43
|
+
const plans = await this.plansRepo.findActive(now);
|
|
44
|
+
if (!plans.length) {
|
|
45
|
+
console.log('📅 No active plans found');
|
|
46
|
+
return;
|
|
61
47
|
}
|
|
62
|
-
|
|
63
|
-
|
|
48
|
+
console.log(`📅 Found ${plans.length} active plans`);
|
|
49
|
+
for (const plan of plans) {
|
|
50
|
+
await this.occurrencesRepo.deleteByPlanIdAfter(plan.id, now.toISOString());
|
|
64
51
|
}
|
|
52
|
+
const allOccurrences = [];
|
|
53
|
+
for (const plan of plans) {
|
|
54
|
+
try {
|
|
55
|
+
const expanded = (0, expandPlan_1.expandPlan)(plan.schedule_rule, now);
|
|
56
|
+
const windowed = (0, windowPlanner_1.selectWindow)(expanded, { windowDays: days, maxPending: 1000 });
|
|
57
|
+
const dbOccurrences = windowed.map(occ => ({
|
|
58
|
+
id: `${plan.id}:${occ.scheduledAt}`,
|
|
59
|
+
plan_id: plan.id,
|
|
60
|
+
scheduled_at: occ.scheduledAt,
|
|
61
|
+
kind: (plan.item_type || 'MED'),
|
|
62
|
+
qty: occ.qty || 1
|
|
63
|
+
}));
|
|
64
|
+
allOccurrences.push(...dbOccurrences);
|
|
65
|
+
console.log(`📅 Plan ${plan.id}: ${dbOccurrences.length} occurrences`);
|
|
66
|
+
}
|
|
67
|
+
catch (error) {
|
|
68
|
+
console.error(`❌ Error expanding plan ${plan.id}:`, error);
|
|
69
|
+
}
|
|
70
|
+
}
|
|
71
|
+
if (allOccurrences.length) {
|
|
72
|
+
await this.occurrencesRepo.insertMany(allOccurrences);
|
|
73
|
+
console.log(`📅 ${allOccurrences.length} occurrences synced`);
|
|
74
|
+
}
|
|
75
|
+
this.eventBus.emit('window:filled', { count: allOccurrences.length });
|
|
65
76
|
}
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
77
|
+
catch (error) {
|
|
78
|
+
console.error('❌ Error filling window:', error);
|
|
79
|
+
throw error;
|
|
69
80
|
}
|
|
70
|
-
|
|
71
|
-
}
|
|
72
|
-
catch (error) {
|
|
73
|
-
console.error('❌ Error filling window:', error);
|
|
74
|
-
throw error;
|
|
75
|
-
}
|
|
81
|
+
});
|
|
76
82
|
}
|
|
77
83
|
async reconcileWithOS(options = {}) {
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
groups.
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
}
|
|
100
|
-
const notifsByOccId = new Map();
|
|
101
|
-
const notifsByGroupKey = new Map();
|
|
102
|
-
for (const notif of scheduledNotifs) {
|
|
103
|
-
const occId = notif.notification?.data?.occurrenceId;
|
|
104
|
-
if (occId) {
|
|
105
|
-
notifsByOccId.set(occId, notif.notification.id);
|
|
106
|
-
}
|
|
107
|
-
const gk = notif.notification?.data?.groupKey;
|
|
108
|
-
if (gk) {
|
|
109
|
-
let count = 0;
|
|
110
|
-
try {
|
|
111
|
-
const occIdsRaw = notif.notification?.data?.occurrenceIds;
|
|
112
|
-
const occIds = Array.isArray(occIdsRaw) ? occIdsRaw : (typeof occIdsRaw === 'string' ? (JSON.parse(occIdsRaw || '[]') || []) : []);
|
|
113
|
-
count = Array.isArray(occIds) ? occIds.length : 0;
|
|
84
|
+
return await this.runSerial(async () => {
|
|
85
|
+
const { horizonDays = 2, includePastDay = false } = options;
|
|
86
|
+
try {
|
|
87
|
+
const pendingOccs = await this.occurrencesRepo.getUpcomingWindow({
|
|
88
|
+
days: Math.max(1, horizonDays),
|
|
89
|
+
limit: 50,
|
|
90
|
+
pastDays: includePastDay ? 1 : 0
|
|
91
|
+
});
|
|
92
|
+
const pending = pendingOccs.filter(occ => occ.status === 'PENDING');
|
|
93
|
+
const scheduledNotifs = await this.notificationDriver.listScheduled();
|
|
94
|
+
console.log(`🔄 Reconciling (horizon=${horizonDays}d, cap=45, group=${this.groupSameTime ? 'on' : 'off'}): ${pending.length} pending, ${scheduledNotifs.length} scheduled in OS`);
|
|
95
|
+
const groups = new Map();
|
|
96
|
+
if (this.groupSameTime) {
|
|
97
|
+
for (const occ of pending) {
|
|
98
|
+
if (occ.kind !== 'MED')
|
|
99
|
+
continue;
|
|
100
|
+
const d = (0, timestamp_1.parseUTC)(occ.scheduled_at);
|
|
101
|
+
const key = `${occ.kind}:${d.getFullYear()}-${(d.getMonth() + 1).toString().padStart(2, '0')}-${d.getDate().toString().padStart(2, '0')}T${d.getHours().toString().padStart(2, '0')}:${d.getMinutes().toString().padStart(2, '0')}`;
|
|
102
|
+
if (!groups.has(key))
|
|
103
|
+
groups.set(key, []);
|
|
104
|
+
groups.get(key).push(occ);
|
|
114
105
|
}
|
|
115
|
-
catch { }
|
|
116
|
-
notifsByGroupKey.set(gk, { id: notif.notification?.id || '', count });
|
|
117
106
|
}
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
if (!occs || occs.length === 0)
|
|
125
|
-
continue;
|
|
126
|
-
const occ = occs.sort((a, b) => (0, timestamp_1.parseUTC)(a.scheduled_at).getTime() - (0, timestamp_1.parseUTC)(b.scheduled_at).getTime())[0];
|
|
127
|
-
const scheduledTime = (0, timestamp_1.parseUTC)(occ.scheduled_at);
|
|
128
|
-
const SAFETY_BUFFER_MS = 2000;
|
|
129
|
-
if (scheduledTime.getTime() <= now.getTime() + SAFETY_BUFFER_MS)
|
|
130
|
-
continue;
|
|
131
|
-
if (occs.length < 2) {
|
|
132
|
-
continue;
|
|
107
|
+
const notifsByOccId = new Map();
|
|
108
|
+
const notifsByGroupKey = new Map();
|
|
109
|
+
for (const notif of scheduledNotifs) {
|
|
110
|
+
const occId = notif.notification?.data?.occurrenceId;
|
|
111
|
+
if (occId) {
|
|
112
|
+
notifsByOccId.set(occId, notif.notification.id);
|
|
133
113
|
}
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
const groupKey = key;
|
|
138
|
-
const legacyDailyKey = `${occ.kind}:HH:${hh}:${mm}`;
|
|
139
|
-
const names = occs.map((o) => o.item_name).filter(Boolean);
|
|
140
|
-
const namesShort = names.slice(0, 2).join(', ') + (names.length > 2 ? ` +${names.length - 2}` : '');
|
|
141
|
-
const title = `💊 ${occs.length} medicamentos agora`;
|
|
142
|
-
const body = namesShort ? `${hh}:${mm} • ${namesShort}` : `${hh}:${mm}`;
|
|
143
|
-
const existing = notifsByGroupKey.get(groupKey);
|
|
144
|
-
if (existing && Number(existing.count || 0) === occs.length) {
|
|
145
|
-
continue;
|
|
146
|
-
}
|
|
114
|
+
const gk = notif.notification?.data?.groupKey;
|
|
115
|
+
if (gk) {
|
|
116
|
+
let count = 0;
|
|
147
117
|
try {
|
|
148
|
-
|
|
118
|
+
const occIdsRaw = notif.notification?.data?.occurrenceIds;
|
|
119
|
+
const occIds = Array.isArray(occIdsRaw) ? occIdsRaw : (typeof occIdsRaw === 'string' ? (JSON.parse(occIdsRaw || '[]') || []) : []);
|
|
120
|
+
count = Array.isArray(occIds) ? occIds.length : 0;
|
|
149
121
|
}
|
|
150
122
|
catch { }
|
|
151
|
-
|
|
152
|
-
|
|
123
|
+
notifsByGroupKey.set(gk, { id: notif.notification?.id || '', count });
|
|
124
|
+
}
|
|
125
|
+
}
|
|
126
|
+
let scheduled = 0;
|
|
127
|
+
let cancelled = 0;
|
|
128
|
+
const now = new Date();
|
|
129
|
+
if (this.groupSameTime && groups.size > 0) {
|
|
130
|
+
for (const [key, occs] of groups) {
|
|
131
|
+
if (!occs || occs.length === 0)
|
|
132
|
+
continue;
|
|
133
|
+
const occ = occs.sort((a, b) => (0, timestamp_1.parseUTC)(a.scheduled_at).getTime() - (0, timestamp_1.parseUTC)(b.scheduled_at).getTime())[0];
|
|
134
|
+
const scheduledTime = (0, timestamp_1.parseUTC)(occ.scheduled_at);
|
|
135
|
+
const SAFETY_BUFFER_MS = 2000;
|
|
136
|
+
if (scheduledTime.getTime() <= now.getTime() + SAFETY_BUFFER_MS)
|
|
137
|
+
continue;
|
|
138
|
+
if (occs.length < 2) {
|
|
139
|
+
continue;
|
|
153
140
|
}
|
|
154
|
-
|
|
155
|
-
|
|
141
|
+
try {
|
|
142
|
+
const hh = scheduledTime.getHours().toString().padStart(2, '0');
|
|
143
|
+
const mm = scheduledTime.getMinutes().toString().padStart(2, '0');
|
|
144
|
+
const groupKey = key;
|
|
145
|
+
const legacyDailyKey = `${occ.kind}:HH:${hh}:${mm}`;
|
|
146
|
+
const names = occs.map((o) => o.item_name).filter(Boolean);
|
|
147
|
+
const namesShort = names.slice(0, 2).join(', ') + (names.length > 2 ? ` +${names.length - 2}` : '');
|
|
148
|
+
const title = `💊 ${occs.length} medicamentos agora`;
|
|
149
|
+
const body = namesShort ? `${hh}:${mm} • ${namesShort}` : `${hh}:${mm}`;
|
|
150
|
+
const existing = notifsByGroupKey.get(groupKey);
|
|
151
|
+
if (existing && Number(existing.count || 0) === occs.length) {
|
|
152
|
+
continue;
|
|
153
|
+
}
|
|
156
154
|
try {
|
|
157
|
-
await this.notificationDriver.
|
|
155
|
+
await this.notificationDriver.cancelTriggerByGroup(groupKey);
|
|
158
156
|
}
|
|
159
157
|
catch { }
|
|
160
|
-
|
|
161
|
-
|
|
162
|
-
|
|
163
|
-
|
|
164
|
-
|
|
165
|
-
|
|
166
|
-
|
|
167
|
-
|
|
168
|
-
|
|
169
|
-
|
|
170
|
-
|
|
171
|
-
|
|
172
|
-
|
|
173
|
-
catch (error) {
|
|
174
|
-
console.error(`❌ Error scheduling group ${key}:`, error);
|
|
175
|
-
}
|
|
176
|
-
}
|
|
177
|
-
const groupedOccIds = new Set();
|
|
178
|
-
for (const occs of groups.values()) {
|
|
179
|
-
if (occs && occs.length >= 2) {
|
|
180
|
-
for (const o of occs)
|
|
181
|
-
groupedOccIds.add(o.id);
|
|
182
|
-
}
|
|
183
|
-
}
|
|
184
|
-
const remainder = pending.filter((o) => o.kind !== 'MED' || !groupedOccIds.has(o.id));
|
|
185
|
-
for (const occ of remainder.slice(0, 45)) {
|
|
186
|
-
const occId = occ.id;
|
|
187
|
-
const isScheduled = notifsByOccId.has(occId);
|
|
188
|
-
const scheduledTime = (0, timestamp_1.parseUTC)(occ.scheduled_at);
|
|
189
|
-
if (scheduledTime.getTime() <= now.getTime()) {
|
|
190
|
-
continue;
|
|
191
|
-
}
|
|
192
|
-
if (!isScheduled) {
|
|
193
|
-
try {
|
|
194
|
-
const reminderOffset = (occ.kind === 'APPT' || occ.kind === 'TEST')
|
|
195
|
-
? await this.getReminderOffset(occ.plan_id)
|
|
196
|
-
: undefined;
|
|
197
|
-
await this.notificationDriver.scheduleTrigger({
|
|
198
|
-
occurrenceId: occId,
|
|
158
|
+
try {
|
|
159
|
+
await this.notificationDriver.cancelTriggerByGroup(legacyDailyKey);
|
|
160
|
+
}
|
|
161
|
+
catch { }
|
|
162
|
+
for (const o of occs) {
|
|
163
|
+
try {
|
|
164
|
+
await this.notificationDriver.cancelTriggerByOccurrence(o.id);
|
|
165
|
+
}
|
|
166
|
+
catch { }
|
|
167
|
+
}
|
|
168
|
+
await this.notificationDriver.scheduleGroupTrigger({
|
|
169
|
+
groupKey,
|
|
170
|
+
occurrenceIds: occs.map((o) => o.id),
|
|
199
171
|
itemType: occ.kind,
|
|
200
172
|
itemId: occ.item_id || occ.plan_id,
|
|
201
|
-
title
|
|
202
|
-
body
|
|
173
|
+
title,
|
|
174
|
+
body,
|
|
203
175
|
scheduledAt: scheduledTime,
|
|
204
|
-
snoozeMin: this.snoozeMinutes
|
|
205
|
-
reminderOffset
|
|
176
|
+
snoozeMin: this.snoozeMinutes
|
|
206
177
|
});
|
|
207
178
|
scheduled++;
|
|
208
179
|
}
|
|
209
180
|
catch (error) {
|
|
210
|
-
console.error(`❌ Error scheduling ${
|
|
181
|
+
console.error(`❌ Error scheduling group ${key}:`, error);
|
|
211
182
|
}
|
|
212
183
|
}
|
|
213
|
-
|
|
214
|
-
|
|
215
|
-
|
|
216
|
-
|
|
217
|
-
|
|
218
|
-
const isScheduled = notifsByOccId.has(occId);
|
|
219
|
-
const scheduledTime = (0, timestamp_1.parseUTC)(occ.scheduled_at);
|
|
220
|
-
if (scheduledTime.getTime() <= now.getTime())
|
|
221
|
-
continue;
|
|
222
|
-
if (!isScheduled) {
|
|
223
|
-
try {
|
|
224
|
-
const reminderOffset = (occ.kind === 'APPT' || occ.kind === 'TEST')
|
|
225
|
-
? await this.getReminderOffset(occ.plan_id)
|
|
226
|
-
: undefined;
|
|
227
|
-
await this.notificationDriver.scheduleTrigger({
|
|
228
|
-
occurrenceId: occId,
|
|
229
|
-
itemType: occ.kind,
|
|
230
|
-
itemId: occ.item_id || occ.plan_id,
|
|
231
|
-
title: this.formatNotificationTitle(occ),
|
|
232
|
-
body: this.formatNotificationBody(occ),
|
|
233
|
-
scheduledAt: scheduledTime,
|
|
234
|
-
snoozeMin: this.snoozeMinutes,
|
|
235
|
-
reminderOffset
|
|
236
|
-
});
|
|
237
|
-
scheduled++;
|
|
184
|
+
const groupedOccIds = new Set();
|
|
185
|
+
for (const occs of groups.values()) {
|
|
186
|
+
if (occs && occs.length >= 2) {
|
|
187
|
+
for (const o of occs)
|
|
188
|
+
groupedOccIds.add(o.id);
|
|
238
189
|
}
|
|
239
|
-
|
|
240
|
-
|
|
190
|
+
}
|
|
191
|
+
const remainder = pending.filter((o) => o.kind !== 'MED' || !groupedOccIds.has(o.id));
|
|
192
|
+
for (const occ of remainder.slice(0, 45)) {
|
|
193
|
+
const occId = occ.id;
|
|
194
|
+
const isScheduled = notifsByOccId.has(occId);
|
|
195
|
+
const scheduledTime = (0, timestamp_1.parseUTC)(occ.scheduled_at);
|
|
196
|
+
if (scheduledTime.getTime() <= now.getTime()) {
|
|
197
|
+
continue;
|
|
198
|
+
}
|
|
199
|
+
if (!isScheduled) {
|
|
200
|
+
try {
|
|
201
|
+
const reminderOffset = (occ.kind === 'APPT' || occ.kind === 'TEST')
|
|
202
|
+
? await this.getReminderOffset(occ.plan_id)
|
|
203
|
+
: undefined;
|
|
204
|
+
await this.notificationDriver.scheduleTrigger({
|
|
205
|
+
occurrenceId: occId,
|
|
206
|
+
itemType: occ.kind,
|
|
207
|
+
itemId: occ.item_id || occ.plan_id,
|
|
208
|
+
title: this.formatNotificationTitle(occ),
|
|
209
|
+
body: this.formatNotificationBody(occ),
|
|
210
|
+
scheduledAt: scheduledTime,
|
|
211
|
+
snoozeMin: this.snoozeMinutes,
|
|
212
|
+
reminderOffset
|
|
213
|
+
});
|
|
214
|
+
scheduled++;
|
|
215
|
+
}
|
|
216
|
+
catch (error) {
|
|
217
|
+
console.error(`❌ Error scheduling ${occId}:`, error);
|
|
218
|
+
}
|
|
241
219
|
}
|
|
242
220
|
}
|
|
243
221
|
}
|
|
244
|
-
|
|
245
|
-
|
|
246
|
-
|
|
247
|
-
|
|
248
|
-
|
|
249
|
-
|
|
250
|
-
|
|
251
|
-
if (!
|
|
222
|
+
else {
|
|
223
|
+
for (const occ of pending.slice(0, 45)) {
|
|
224
|
+
const occId = occ.id;
|
|
225
|
+
const isScheduled = notifsByOccId.has(occId);
|
|
226
|
+
const scheduledTime = (0, timestamp_1.parseUTC)(occ.scheduled_at);
|
|
227
|
+
if (scheduledTime.getTime() <= now.getTime())
|
|
228
|
+
continue;
|
|
229
|
+
if (!isScheduled) {
|
|
230
|
+
try {
|
|
231
|
+
const reminderOffset = (occ.kind === 'APPT' || occ.kind === 'TEST')
|
|
232
|
+
? await this.getReminderOffset(occ.plan_id)
|
|
233
|
+
: undefined;
|
|
234
|
+
await this.notificationDriver.scheduleTrigger({
|
|
235
|
+
occurrenceId: occId,
|
|
236
|
+
itemType: occ.kind,
|
|
237
|
+
itemId: occ.item_id || occ.plan_id,
|
|
238
|
+
title: this.formatNotificationTitle(occ),
|
|
239
|
+
body: this.formatNotificationBody(occ),
|
|
240
|
+
scheduledAt: scheduledTime,
|
|
241
|
+
snoozeMin: this.snoozeMinutes,
|
|
242
|
+
reminderOffset
|
|
243
|
+
});
|
|
244
|
+
scheduled++;
|
|
245
|
+
}
|
|
246
|
+
catch (error) {
|
|
247
|
+
console.error(`❌ Error scheduling ${occId}:`, error);
|
|
248
|
+
}
|
|
249
|
+
}
|
|
250
|
+
}
|
|
251
|
+
}
|
|
252
|
+
try {
|
|
253
|
+
if (this.groupSameTime) {
|
|
254
|
+
const validGroupKeys = new Set();
|
|
255
|
+
for (const key of groups.keys())
|
|
256
|
+
validGroupKeys.add(key);
|
|
257
|
+
for (const [gk] of notifsByGroupKey) {
|
|
258
|
+
if (!validGroupKeys.has(gk)) {
|
|
259
|
+
try {
|
|
260
|
+
await this.notificationDriver.cancelTriggerByGroup(gk);
|
|
261
|
+
cancelled++;
|
|
262
|
+
}
|
|
263
|
+
catch { }
|
|
264
|
+
}
|
|
265
|
+
}
|
|
266
|
+
}
|
|
267
|
+
else {
|
|
268
|
+
for (const [gk] of notifsByGroupKey) {
|
|
252
269
|
try {
|
|
253
270
|
await this.notificationDriver.cancelTriggerByGroup(gk);
|
|
254
271
|
cancelled++;
|
|
@@ -257,94 +274,85 @@ class WindowScheduler {
|
|
|
257
274
|
}
|
|
258
275
|
}
|
|
259
276
|
}
|
|
260
|
-
|
|
261
|
-
|
|
277
|
+
catch { }
|
|
278
|
+
try {
|
|
279
|
+
const snoozedToCheck = pendingOccs.filter(o => String(o.last_action || '').toUpperCase() === 'SNOOZED');
|
|
280
|
+
for (const occ of snoozedToCheck) {
|
|
281
|
+
const lastAt = occ.last_action_at ? new Date(occ.last_action_at) : null;
|
|
282
|
+
let snoozeMin = 0;
|
|
262
283
|
try {
|
|
263
|
-
|
|
264
|
-
|
|
284
|
+
const payload = occ.last_action_payload ? JSON.parse(occ.last_action_payload) : null;
|
|
285
|
+
snoozeMin = Number(payload?.snoozeMin || 0);
|
|
265
286
|
}
|
|
266
287
|
catch { }
|
|
288
|
+
if (!lastAt || !snoozeMin || Number.isNaN(snoozeMin))
|
|
289
|
+
continue;
|
|
290
|
+
const snoozeUntil = new Date(lastAt.getTime() + snoozeMin * 60 * 1000);
|
|
291
|
+
if (snoozeUntil.getTime() <= now.getTime())
|
|
292
|
+
continue;
|
|
293
|
+
const isScheduled = notifsByOccId.has(occ.id);
|
|
294
|
+
if (!isScheduled) {
|
|
295
|
+
try {
|
|
296
|
+
await this.notificationDriver.scheduleTrigger({
|
|
297
|
+
occurrenceId: occ.id,
|
|
298
|
+
itemType: occ.kind,
|
|
299
|
+
itemId: occ.item_id || occ.plan_id,
|
|
300
|
+
title: this.formatNotificationTitle(occ),
|
|
301
|
+
body: this.formatNotificationBody(occ),
|
|
302
|
+
scheduledAt: snoozeUntil,
|
|
303
|
+
snoozeMin: this.snoozeMinutes
|
|
304
|
+
});
|
|
305
|
+
scheduled++;
|
|
306
|
+
console.log('🔁 Snooze re-scheduled by reconcile for', occ.id);
|
|
307
|
+
}
|
|
308
|
+
catch (e) {
|
|
309
|
+
console.error('❌ Error re-scheduling snooze', occ.id, e);
|
|
310
|
+
}
|
|
311
|
+
}
|
|
267
312
|
}
|
|
268
313
|
}
|
|
269
|
-
|
|
270
|
-
|
|
271
|
-
|
|
272
|
-
|
|
273
|
-
|
|
274
|
-
|
|
275
|
-
|
|
276
|
-
try {
|
|
277
|
-
const payload = occ.last_action_payload ? JSON.parse(occ.last_action_payload) : null;
|
|
278
|
-
snoozeMin = Number(payload?.snoozeMin || 0);
|
|
279
|
-
}
|
|
280
|
-
catch { }
|
|
281
|
-
if (!lastAt || !snoozeMin || Number.isNaN(snoozeMin))
|
|
282
|
-
continue;
|
|
283
|
-
const snoozeUntil = new Date(lastAt.getTime() + snoozeMin * 60 * 1000);
|
|
284
|
-
if (snoozeUntil.getTime() <= now.getTime())
|
|
285
|
-
continue;
|
|
286
|
-
const isScheduled = notifsByOccId.has(occ.id);
|
|
287
|
-
if (!isScheduled) {
|
|
314
|
+
catch { }
|
|
315
|
+
const validOccIds = new Set(pending.map(occ => occ.id));
|
|
316
|
+
try {
|
|
317
|
+
const snoozedValid = pendingOccs.filter(o => String(o.last_action || '').toUpperCase() === 'SNOOZED');
|
|
318
|
+
for (const occ of snoozedValid) {
|
|
319
|
+
const lastAt = occ.last_action_at ? new Date(occ.last_action_at) : null;
|
|
320
|
+
let snoozeMin = 0;
|
|
288
321
|
try {
|
|
289
|
-
|
|
290
|
-
|
|
291
|
-
itemType: occ.kind,
|
|
292
|
-
itemId: occ.item_id || occ.plan_id,
|
|
293
|
-
title: this.formatNotificationTitle(occ),
|
|
294
|
-
body: this.formatNotificationBody(occ),
|
|
295
|
-
scheduledAt: snoozeUntil,
|
|
296
|
-
snoozeMin: this.snoozeMinutes
|
|
297
|
-
});
|
|
298
|
-
scheduled++;
|
|
299
|
-
console.log('🔁 Snooze re-scheduled by reconcile for', occ.id);
|
|
322
|
+
const payload = occ.last_action_payload ? JSON.parse(occ.last_action_payload) : null;
|
|
323
|
+
snoozeMin = Number(payload?.snoozeMin || 0);
|
|
300
324
|
}
|
|
301
|
-
catch
|
|
302
|
-
|
|
325
|
+
catch { }
|
|
326
|
+
if (!lastAt || !snoozeMin || Number.isNaN(snoozeMin))
|
|
327
|
+
continue;
|
|
328
|
+
const snoozeUntil = new Date(lastAt.getTime() + snoozeMin * 60 * 1000);
|
|
329
|
+
if (snoozeUntil.getTime() > now.getTime()) {
|
|
330
|
+
validOccIds.add(occ.id);
|
|
303
331
|
}
|
|
304
332
|
}
|
|
305
333
|
}
|
|
306
|
-
|
|
307
|
-
|
|
308
|
-
|
|
309
|
-
|
|
310
|
-
|
|
311
|
-
|
|
312
|
-
|
|
313
|
-
|
|
314
|
-
|
|
315
|
-
|
|
316
|
-
snoozeMin = Number(payload?.snoozeMin || 0);
|
|
317
|
-
}
|
|
318
|
-
catch { }
|
|
319
|
-
if (!lastAt || !snoozeMin || Number.isNaN(snoozeMin))
|
|
320
|
-
continue;
|
|
321
|
-
const snoozeUntil = new Date(lastAt.getTime() + snoozeMin * 60 * 1000);
|
|
322
|
-
if (snoozeUntil.getTime() > now.getTime()) {
|
|
323
|
-
validOccIds.add(occ.id);
|
|
334
|
+
catch { }
|
|
335
|
+
for (const [occId] of notifsByOccId) {
|
|
336
|
+
if (!validOccIds.has(occId)) {
|
|
337
|
+
try {
|
|
338
|
+
await this.notificationDriver.cancelTriggerByOccurrence(occId);
|
|
339
|
+
cancelled++;
|
|
340
|
+
}
|
|
341
|
+
catch (error) {
|
|
342
|
+
console.error(`❌ Error canceling ${occId}:`, error);
|
|
343
|
+
}
|
|
324
344
|
}
|
|
325
345
|
}
|
|
326
|
-
|
|
327
|
-
|
|
328
|
-
for (const [occId] of notifsByOccId) {
|
|
329
|
-
if (!validOccIds.has(occId)) {
|
|
330
|
-
try {
|
|
331
|
-
await this.notificationDriver.cancelTriggerByOccurrence(occId);
|
|
332
|
-
cancelled++;
|
|
333
|
-
}
|
|
334
|
-
catch (error) {
|
|
335
|
-
console.error(`❌ Error canceling ${occId}:`, error);
|
|
336
|
-
}
|
|
346
|
+
if (scheduled > 0 || cancelled > 0) {
|
|
347
|
+
console.log(`🔄 Reconciliation complete (horizon=${horizonDays}d, cap=45): ${scheduled} scheduled, ${cancelled} canceled`);
|
|
337
348
|
}
|
|
349
|
+
this.eventBus.emit('notifications:reconciled', { scheduled, cancelled });
|
|
338
350
|
}
|
|
339
|
-
|
|
340
|
-
console.
|
|
351
|
+
catch (error) {
|
|
352
|
+
console.error('❌ Error in reconciliation:', error);
|
|
353
|
+
throw error;
|
|
341
354
|
}
|
|
342
|
-
|
|
343
|
-
}
|
|
344
|
-
catch (error) {
|
|
345
|
-
console.error('❌ Error in reconciliation:', error);
|
|
346
|
-
throw error;
|
|
347
|
-
}
|
|
355
|
+
});
|
|
348
356
|
}
|
|
349
357
|
async updateWindow(options = {}) {
|
|
350
358
|
await this.fillWindow(options);
|
|
@@ -394,4 +402,5 @@ class WindowScheduler {
|
|
|
394
402
|
}
|
|
395
403
|
}
|
|
396
404
|
exports.WindowScheduler = WindowScheduler;
|
|
405
|
+
WindowScheduler.opQueue = Promise.resolve();
|
|
397
406
|
//# sourceMappingURL=WindowScheduler.js.map
|