nodebb-plugin-onekite-calendar 2.0.30 → 2.0.31
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/CHANGELOG.md +10 -0
- package/lib/admin.js +1 -14
- package/lib/api.js +61 -8
- package/lib/helloassoWebhook.js +1 -29
- package/lib/scheduler.js +64 -72
- package/lib/utils.js +70 -0
- package/lib/widgets.js +23 -1
- package/package.json +1 -1
- package/pkg/package/public/client.js +28 -1
- package/plugin.json +1 -1
- package/public/client.js +28 -1
package/CHANGELOG.md
CHANGED
|
@@ -1,5 +1,15 @@
|
|
|
1
1
|
# Changelog – calendar-onekite
|
|
2
2
|
|
|
3
|
+
## 1.3.28
|
|
4
|
+
- Perf (temps réel) : debounce global des `refetchEvents()` (calendrier + widget) et **skip** quand l’onglet est masqué (refetch unique au retour de visibilité).
|
|
5
|
+
- Perf/Robustesse (multi-instance) : ajout d’un **lock distribué Redis** sur le tick scheduler (un seul runner exécute le cycle à un instant donné).
|
|
6
|
+
- Perf (API events) : cache Redis très court (**TTL 2s**) par utilisateur/permissions sur l’endpoint events pour absorber les rafales.
|
|
7
|
+
|
|
8
|
+
## 1.3.27
|
|
9
|
+
- Refactor : ajout d'un module `lib/utils.js` (format dates FR, lecture settings ACP, helpers listes/UIDs) pour supprimer les duplications.
|
|
10
|
+
- Cleanup : suppression d'un doublon de fonctions dans `helloassoWebhook.js` (formatFR + getReservationIdFromPayload).
|
|
11
|
+
- Perf : scheduler optimisé (chargement **batch** des réservations via `getReservations()` au lieu d'un `getReservation()` par id).
|
|
12
|
+
|
|
3
13
|
## 1.3.26
|
|
4
14
|
- Scheduler : garde "send-once" rendue **atomique** en multi-instance (priorité à `db.incrObjectField(key, rid)`), pour garantir qu'au moins une instance envoie les emails/Discord avant suppression.
|
|
5
15
|
- Scheduler : en cas d'erreur DB sur la garde, on **privilégie l'envoi** plutôt que "expire sans email" (dernier recours : autoriser l'envoi même si la garde échoue).
|
package/lib/admin.js
CHANGED
|
@@ -3,20 +3,7 @@
|
|
|
3
3
|
const meta = require.main.require('./src/meta');
|
|
4
4
|
const user = require.main.require('./src/user');
|
|
5
5
|
const emailer = require.main.require('./src/emailer');
|
|
6
|
-
const
|
|
7
|
-
|
|
8
|
-
function forumBaseUrl() {
|
|
9
|
-
const base = String(nconf.get('url') || '').trim().replace(/\/$/, '');
|
|
10
|
-
return base;
|
|
11
|
-
}
|
|
12
|
-
|
|
13
|
-
function formatFR(tsOrIso) {
|
|
14
|
-
const d = new Date(typeof tsOrIso === 'string' && /^[0-9]+$/.test(tsOrIso) ? parseInt(tsOrIso, 10) : tsOrIso);
|
|
15
|
-
const dd = String(d.getDate()).padStart(2, '0');
|
|
16
|
-
const mm = String(d.getMonth() + 1).padStart(2, '0');
|
|
17
|
-
const yyyy = d.getFullYear();
|
|
18
|
-
return `${dd}/${mm}/${yyyy}`;
|
|
19
|
-
}
|
|
6
|
+
const { forumBaseUrl, formatFR } = require('./utils');
|
|
20
7
|
|
|
21
8
|
function buildHelloAssoItemName(baseLabel, itemNames, start, end) {
|
|
22
9
|
const base = String(baseLabel || 'Réservation matériel Onekite').trim();
|
package/lib/api.js
CHANGED
|
@@ -9,8 +9,11 @@ const user = require.main.require('./src/user');
|
|
|
9
9
|
const groups = require.main.require('./src/groups');
|
|
10
10
|
const db = require.main.require('./src/database');
|
|
11
11
|
const logger = require.main.require('./src/logger');
|
|
12
|
+
let cache = null;
|
|
13
|
+
try { cache = require.main.require('./src/cache'); } catch (e) { cache = null; }
|
|
12
14
|
|
|
13
15
|
const dbLayer = require('./db');
|
|
16
|
+
const { formatFR } = require('./utils');
|
|
14
17
|
|
|
15
18
|
// Resolve group identifiers from ACP.
|
|
16
19
|
// Admins may enter a group *name* ("Test") or a *slug* ("onekite-ffvl-2026").
|
|
@@ -177,14 +180,6 @@ function overlap(aStart, aEnd, bStart, bEnd) {
|
|
|
177
180
|
}
|
|
178
181
|
|
|
179
182
|
|
|
180
|
-
function formatFR(tsOrIso) {
|
|
181
|
-
const d = new Date(typeof tsOrIso === 'string' && /^[0-9]+$/.test(tsOrIso) ? parseInt(tsOrIso, 10) : tsOrIso);
|
|
182
|
-
const dd = String(d.getDate()).padStart(2, '0');
|
|
183
|
-
const mm = String(d.getMonth() + 1).padStart(2, '0');
|
|
184
|
-
const yyyy = d.getFullYear();
|
|
185
|
-
return `${dd}/${mm}/${yyyy}`;
|
|
186
|
-
}
|
|
187
|
-
|
|
188
183
|
function buildHelloAssoItemName(baseLabel, itemNames, start, end) {
|
|
189
184
|
const base = String(baseLabel || '').trim();
|
|
190
185
|
const items = Array.isArray(itemNames) ? itemNames.map((s) => String(s || '').trim()).filter(Boolean) : [];
|
|
@@ -484,6 +479,42 @@ function computeEtag(payload) {
|
|
|
484
479
|
return `W/"${hash}"`;
|
|
485
480
|
}
|
|
486
481
|
|
|
482
|
+
async function cacheGet(key) {
|
|
483
|
+
try {
|
|
484
|
+
if (!cache) return null;
|
|
485
|
+
if (typeof cache.get === 'function') {
|
|
486
|
+
if (cache.get.length >= 2) {
|
|
487
|
+
return await new Promise((resolve) => {
|
|
488
|
+
cache.get(key, (err, data) => resolve(err ? null : data));
|
|
489
|
+
});
|
|
490
|
+
}
|
|
491
|
+
return await cache.get(key);
|
|
492
|
+
}
|
|
493
|
+
} catch (e) {}
|
|
494
|
+
return null;
|
|
495
|
+
}
|
|
496
|
+
|
|
497
|
+
async function cacheSet(key, value, ttlSeconds) {
|
|
498
|
+
try {
|
|
499
|
+
if (!cache) return;
|
|
500
|
+
const ttl = Math.max(1, parseInt(ttlSeconds, 10) || 2);
|
|
501
|
+
if (typeof cache.set === 'function') {
|
|
502
|
+
if (cache.set.length >= 4) {
|
|
503
|
+
await new Promise((resolve) => {
|
|
504
|
+
cache.set(key, value, ttl, (err) => resolve(!err));
|
|
505
|
+
});
|
|
506
|
+
return;
|
|
507
|
+
}
|
|
508
|
+
// Some NodeBB versions accept an options object.
|
|
509
|
+
try {
|
|
510
|
+
await cache.set(key, value, { ttl });
|
|
511
|
+
return;
|
|
512
|
+
} catch (e) {}
|
|
513
|
+
await cache.set(key, value, ttl);
|
|
514
|
+
}
|
|
515
|
+
} catch (e) {}
|
|
516
|
+
}
|
|
517
|
+
|
|
487
518
|
api.getEvents = async function (req, res) {
|
|
488
519
|
const qStartRaw = (req && req.query && req.query.start !== undefined) ? String(req.query.start).trim() : '';
|
|
489
520
|
const qEndRaw = (req && req.query && req.query.end !== undefined) ? String(req.query.end).trim() : '';
|
|
@@ -503,6 +534,22 @@ api.getEvents = async function (req, res) {
|
|
|
503
534
|
const canSpecialCreate = req.uid ? await canCreateSpecial(req.uid, settings) : false;
|
|
504
535
|
const canSpecialDelete = req.uid ? await canDeleteSpecial(req.uid, settings) : false;
|
|
505
536
|
|
|
537
|
+
// Ultra-short Redis-backed cache (2s) to absorb bursts and reduce redundant renders.
|
|
538
|
+
// Keyed per uid + permissions because payload can include private fields.
|
|
539
|
+
const uidKey = req.uid ? String(req.uid) : '0';
|
|
540
|
+
const cacheKey = `calendar-onekite:events:${uidKey}:${canMod ? 'm' : 'u'}:${widgetMode ? 'w' : 'p'}:${qStartYmd || startTs}:${qEndYmd || endTs}`;
|
|
541
|
+
try {
|
|
542
|
+
const cached = await cacheGet(cacheKey);
|
|
543
|
+
if (cached && cached.payload && cached.etag) {
|
|
544
|
+
res.setHeader('ETag', cached.etag);
|
|
545
|
+
res.setHeader('Cache-Control', 'private, max-age=0, must-revalidate');
|
|
546
|
+
if (String(req.headers['if-none-match'] || '') === String(cached.etag)) {
|
|
547
|
+
return res.status(304).end();
|
|
548
|
+
}
|
|
549
|
+
return res.json(cached.payload);
|
|
550
|
+
}
|
|
551
|
+
} catch (e) {}
|
|
552
|
+
|
|
506
553
|
// Fetch a wider window because an event can start before the query range
|
|
507
554
|
// and still overlap.
|
|
508
555
|
const wideStart = Math.max(0, startTs - 366 * 24 * 3600 * 1000);
|
|
@@ -641,6 +688,12 @@ api.getEvents = async function (req, res) {
|
|
|
641
688
|
});
|
|
642
689
|
|
|
643
690
|
const etag = computeEtag(out);
|
|
691
|
+
|
|
692
|
+
// Store in cache for a very short period (burst absorption).
|
|
693
|
+
try {
|
|
694
|
+
await cacheSet(cacheKey, { etag, payload: out }, 2);
|
|
695
|
+
} catch (e) {}
|
|
696
|
+
|
|
644
697
|
res.setHeader('ETag', etag);
|
|
645
698
|
res.setHeader('Cache-Control', 'private, max-age=0, must-revalidate');
|
|
646
699
|
if (String(req.headers['if-none-match'] || '') === etag) {
|
package/lib/helloassoWebhook.js
CHANGED
|
@@ -10,6 +10,7 @@ const dbLayer = require('./db');
|
|
|
10
10
|
const helloasso = require('./helloasso');
|
|
11
11
|
const discord = require('./discord');
|
|
12
12
|
const realtime = require('./realtime');
|
|
13
|
+
const { formatFR } = require('./utils');
|
|
13
14
|
|
|
14
15
|
async function auditLog(action, actorUid, payload) {
|
|
15
16
|
try {
|
|
@@ -49,14 +50,6 @@ async function sendEmail(template, uid, subject, data) {
|
|
|
49
50
|
}
|
|
50
51
|
}
|
|
51
52
|
|
|
52
|
-
function formatFR(tsOrIso) {
|
|
53
|
-
const d = new Date(tsOrIso);
|
|
54
|
-
const dd = String(d.getDate()).padStart(2, '0');
|
|
55
|
-
const mm = String(d.getMonth() + 1).padStart(2, '0');
|
|
56
|
-
const yyyy = d.getFullYear();
|
|
57
|
-
return `${dd}/${mm}/${yyyy}`;
|
|
58
|
-
}
|
|
59
|
-
|
|
60
53
|
function getReservationIdFromPayload(payload) {
|
|
61
54
|
try {
|
|
62
55
|
const data = payload && payload.data ? payload.data : null;
|
|
@@ -186,27 +179,6 @@ function isConfirmedPayment(payload) {
|
|
|
186
179
|
}
|
|
187
180
|
}
|
|
188
181
|
|
|
189
|
-
function formatFR(tsOrIso) {
|
|
190
|
-
const d = new Date(tsOrIso);
|
|
191
|
-
const dd = String(d.getDate()).padStart(2, '0');
|
|
192
|
-
const mm = String(d.getMonth() + 1).padStart(2, '0');
|
|
193
|
-
const yyyy = d.getFullYear();
|
|
194
|
-
return `${dd}/${mm}/${yyyy}`;
|
|
195
|
-
}
|
|
196
|
-
|
|
197
|
-
function getReservationIdFromPayload(payload) {
|
|
198
|
-
try {
|
|
199
|
-
const m = payload && payload.data ? payload.data.meta : null;
|
|
200
|
-
if (!m) return null;
|
|
201
|
-
if (typeof m === 'object' && m.reservationId) return String(m.reservationId);
|
|
202
|
-
if (typeof m === 'object' && m.reservationID) return String(m.reservationID);
|
|
203
|
-
return null;
|
|
204
|
-
} catch (e) {
|
|
205
|
-
return null;
|
|
206
|
-
}
|
|
207
|
-
}
|
|
208
|
-
|
|
209
|
-
|
|
210
182
|
function getCheckoutIntentIdFromPayload(payload) {
|
|
211
183
|
try {
|
|
212
184
|
const data = payload && payload.data ? payload.data : null;
|
package/lib/scheduler.js
CHANGED
|
@@ -7,9 +7,44 @@ const discord = require('./discord');
|
|
|
7
7
|
const realtime = require('./realtime');
|
|
8
8
|
const nconf = require.main.require('nconf');
|
|
9
9
|
const groups = require.main.require('./src/groups');
|
|
10
|
+
const utils = require('./utils');
|
|
11
|
+
let redis = null;
|
|
12
|
+
try { redis = require.main.require('./src/redis'); } catch (e) { redis = null; }
|
|
10
13
|
|
|
11
14
|
let timer = null;
|
|
12
15
|
|
|
16
|
+
// Distributed lock (Redis) to avoid running the scheduler on multiple NodeBB instances.
|
|
17
|
+
// Safe no-op if Redis is not available.
|
|
18
|
+
async function acquireSchedulerLock(ttlMs) {
|
|
19
|
+
const key = 'calendar-onekite:scheduler:lock';
|
|
20
|
+
const value = `${process.pid}:${Date.now()}`;
|
|
21
|
+
const ttl = Math.max(1000, parseInt(ttlMs, 10) || 55000);
|
|
22
|
+
|
|
23
|
+
// Prefer Redis SET NX PX.
|
|
24
|
+
try {
|
|
25
|
+
const client = redis && (redis.client || redis);
|
|
26
|
+
if (client && typeof client.set === 'function') {
|
|
27
|
+
const res = await new Promise((resolve, reject) => {
|
|
28
|
+
// node_redis style
|
|
29
|
+
client.set(key, value, 'NX', 'PX', ttl, (err, out) => {
|
|
30
|
+
if (err) return reject(err);
|
|
31
|
+
resolve(out);
|
|
32
|
+
});
|
|
33
|
+
});
|
|
34
|
+
return String(res || '').toUpperCase() === 'OK';
|
|
35
|
+
}
|
|
36
|
+
} catch (e) {
|
|
37
|
+
// ignore and fall back
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
// Fallback: in-process lock only.
|
|
41
|
+
if (global.__onekiteSchedulerInProcessLockUntil && Date.now() < global.__onekiteSchedulerInProcessLockUntil) {
|
|
42
|
+
return false;
|
|
43
|
+
}
|
|
44
|
+
global.__onekiteSchedulerInProcessLockUntil = Date.now() + ttl;
|
|
45
|
+
return true;
|
|
46
|
+
}
|
|
47
|
+
|
|
13
48
|
// Some NodeBB database adapters don't expose setAdd/setRemove helpers.
|
|
14
49
|
// Use a safe "add once" guard to avoid crashing the scheduler.
|
|
15
50
|
async function addOnce(key, value) {
|
|
@@ -51,25 +86,7 @@ async function addOnce(key, value) {
|
|
|
51
86
|
}
|
|
52
87
|
}
|
|
53
88
|
|
|
54
|
-
|
|
55
|
-
const v = settings && Object.prototype.hasOwnProperty.call(settings, key) ? settings[key] : undefined;
|
|
56
|
-
if (v == null || v === '') return fallback;
|
|
57
|
-
if (typeof v === 'object' && v && typeof v.value !== 'undefined') return v.value;
|
|
58
|
-
return v;
|
|
59
|
-
}
|
|
60
|
-
|
|
61
|
-
function formatFR(ts) {
|
|
62
|
-
const d = new Date(ts);
|
|
63
|
-
const dd = String(d.getDate()).padStart(2, '0');
|
|
64
|
-
const mm = String(d.getMonth() + 1).padStart(2, '0');
|
|
65
|
-
const yyyy = d.getFullYear();
|
|
66
|
-
return `${dd}/${mm}/${yyyy}`;
|
|
67
|
-
}
|
|
68
|
-
|
|
69
|
-
function forumBaseUrl() {
|
|
70
|
-
const base = String(nconf.get('url') || '').trim().replace(/\/$/, '');
|
|
71
|
-
return base;
|
|
72
|
-
}
|
|
89
|
+
const { getSetting, formatFR, forumBaseUrl, normalizeAllowedGroups, normalizeUids, arrayifyNames } = utils;
|
|
73
90
|
|
|
74
91
|
// Resolve group identifiers from ACP (name or slug) and return UIDs.
|
|
75
92
|
async function getMembersByGroupIdentifier(groupIdentifier) {
|
|
@@ -111,30 +128,7 @@ async function getMembersByGroupIdentifier(groupIdentifier) {
|
|
|
111
128
|
return Array.isArray(members) ? members : [];
|
|
112
129
|
}
|
|
113
130
|
|
|
114
|
-
|
|
115
|
-
if (!Array.isArray(members)) return [];
|
|
116
|
-
const out = [];
|
|
117
|
-
for (const m of members) {
|
|
118
|
-
if (Number.isInteger(m)) { out.push(m); continue; }
|
|
119
|
-
if (typeof m === 'string' && m.trim() && !Number.isNaN(parseInt(m, 10))) { out.push(parseInt(m, 10)); continue; }
|
|
120
|
-
if (m && typeof m === 'object' && (Number.isInteger(m.uid) || (typeof m.uid === 'string' && m.uid.trim()))) {
|
|
121
|
-
const u = Number.isInteger(m.uid) ? m.uid : parseInt(m.uid, 10);
|
|
122
|
-
if (!Number.isNaN(u)) out.push(u);
|
|
123
|
-
}
|
|
124
|
-
}
|
|
125
|
-
return Array.from(new Set(out)).filter((u) => Number.isInteger(u) && u > 0);
|
|
126
|
-
}
|
|
127
|
-
|
|
128
|
-
function normalizeAllowedGroups(raw) {
|
|
129
|
-
if (!raw) return [];
|
|
130
|
-
if (Array.isArray(raw)) {
|
|
131
|
-
return raw.map((s) => String(s || '').trim()).filter(Boolean);
|
|
132
|
-
}
|
|
133
|
-
return String(raw)
|
|
134
|
-
.split(',')
|
|
135
|
-
.map((s) => String(s || '').trim())
|
|
136
|
-
.filter(Boolean);
|
|
137
|
-
}
|
|
131
|
+
// normalizeUids/normalizeAllowedGroups are provided by utils
|
|
138
132
|
|
|
139
133
|
async function getValidatorUids(settings) {
|
|
140
134
|
const out = new Set();
|
|
@@ -189,12 +183,14 @@ async function expirePending() {
|
|
|
189
183
|
const validatorUids = validatorReminderMins > 0 ? await getValidatorUids(settings) : [];
|
|
190
184
|
|
|
191
185
|
const ids = await dbLayer.listAllReservationIds(5000);
|
|
192
|
-
if (!ids || !ids.length)
|
|
193
|
-
|
|
194
|
-
|
|
186
|
+
if (!ids || !ids.length) return;
|
|
187
|
+
|
|
188
|
+
// Batch load to avoid N+1 queries on Mongo/Redis adapters.
|
|
189
|
+
const reservations = await dbLayer.getReservations(ids);
|
|
195
190
|
|
|
196
|
-
for (
|
|
197
|
-
const
|
|
191
|
+
for (let i = 0; i < ids.length; i += 1) {
|
|
192
|
+
const rid = ids[i];
|
|
193
|
+
const resv = reservations[i];
|
|
198
194
|
if (!resv || resv.status !== 'pending') {
|
|
199
195
|
continue;
|
|
200
196
|
}
|
|
@@ -213,7 +209,7 @@ async function expirePending() {
|
|
|
213
209
|
await sendEmail('calendar-onekite_validator_reminder', vuid, 'Location matériel - Demande en attente', {
|
|
214
210
|
rid,
|
|
215
211
|
requesterUsername,
|
|
216
|
-
itemNames: (
|
|
212
|
+
itemNames: arrayifyNames(resv),
|
|
217
213
|
dateRange: `Du ${formatFR(resv.start)} au ${formatFR(resv.end)}`,
|
|
218
214
|
adminUrl,
|
|
219
215
|
kind: 'pending',
|
|
@@ -238,7 +234,7 @@ async function expirePending() {
|
|
|
238
234
|
await sendEmail('calendar-onekite_expired', requesterUid, 'Location matériel - Demande expirée', {
|
|
239
235
|
uid: requesterUid,
|
|
240
236
|
username: requesterUsername,
|
|
241
|
-
itemNames: (
|
|
237
|
+
itemNames: arrayifyNames(resv),
|
|
242
238
|
dateRange: `Du ${formatFR(resv.start)} au ${formatFR(resv.end)}`,
|
|
243
239
|
delayMinutes: holdMins,
|
|
244
240
|
reason,
|
|
@@ -252,7 +248,7 @@ async function expirePending() {
|
|
|
252
248
|
await sendEmail('calendar-onekite_validator_expired', vuid, 'Location matériel - Demande expirée', {
|
|
253
249
|
rid,
|
|
254
250
|
requesterUsername,
|
|
255
|
-
itemNames: (
|
|
251
|
+
itemNames: arrayifyNames(resv),
|
|
256
252
|
dateRange: `Du ${formatFR(resv.start)} au ${formatFR(resv.end)}`,
|
|
257
253
|
adminUrl,
|
|
258
254
|
reason,
|
|
@@ -308,21 +304,16 @@ async function processAwaitingPayment() {
|
|
|
308
304
|
}
|
|
309
305
|
}
|
|
310
306
|
|
|
311
|
-
function formatFR(ts) {
|
|
312
|
-
const d = new Date(ts);
|
|
313
|
-
const dd = String(d.getDate()).padStart(2, '0');
|
|
314
|
-
const mm = String(d.getMonth() + 1).padStart(2, '0');
|
|
315
|
-
const yyyy = d.getFullYear();
|
|
316
|
-
return `${dd}/${mm}/${yyyy}`;
|
|
317
|
-
}
|
|
318
|
-
|
|
319
307
|
const adminUrl = (() => {
|
|
320
308
|
const base = forumBaseUrl();
|
|
321
309
|
return base ? `${base}/admin/plugins/calendar-onekite` : '/admin/plugins/calendar-onekite';
|
|
322
310
|
})();
|
|
323
311
|
|
|
324
|
-
|
|
325
|
-
|
|
312
|
+
const reservations = await dbLayer.getReservations(ids);
|
|
313
|
+
|
|
314
|
+
for (let i = 0; i < ids.length; i += 1) {
|
|
315
|
+
const rid = ids[i];
|
|
316
|
+
const r = reservations[i];
|
|
326
317
|
if (!r || r.status !== 'awaiting_payment') continue;
|
|
327
318
|
|
|
328
319
|
const approvedAt = parseInt(r.approvedAt || r.validatedAt || 0, 10) || 0;
|
|
@@ -349,8 +340,8 @@ async function processAwaitingPayment() {
|
|
|
349
340
|
await sendEmail('calendar-onekite_reminder', toUid, 'Location matériel - Rappel', {
|
|
350
341
|
uid: toUid,
|
|
351
342
|
username: (u && u.username) ? u.username : '',
|
|
352
|
-
itemName: (
|
|
353
|
-
itemNames: (
|
|
343
|
+
itemName: arrayifyNames(r).join(', '),
|
|
344
|
+
itemNames: arrayifyNames(r),
|
|
354
345
|
dateRange: `Du ${formatFR(r.start)} au ${formatFR(r.end)}`,
|
|
355
346
|
paymentUrl: r.paymentUrl || '',
|
|
356
347
|
delayMinutes: holdMins,
|
|
@@ -383,8 +374,8 @@ async function processAwaitingPayment() {
|
|
|
383
374
|
await sendEmail('calendar-onekite_expired', toUid, 'Location matériel - Demande expirée', {
|
|
384
375
|
uid: toUid,
|
|
385
376
|
username: (u && u.username) ? u.username : '',
|
|
386
|
-
itemName: (
|
|
387
|
-
itemNames: (
|
|
377
|
+
itemName: arrayifyNames(r).join(', '),
|
|
378
|
+
itemNames: arrayifyNames(r),
|
|
388
379
|
dateRange: `Du ${formatFR(r.start)} au ${formatFR(r.end)}`,
|
|
389
380
|
delayMinutes: holdMins,
|
|
390
381
|
reason: 'Paiement non reçu dans le temps imparti.',
|
|
@@ -398,7 +389,7 @@ async function processAwaitingPayment() {
|
|
|
398
389
|
await sendEmail('calendar-onekite_validator_expired', vuid, 'Location matériel - Demande expirée', {
|
|
399
390
|
rid: r.rid || rid,
|
|
400
391
|
requesterUsername: (u && u.username) ? u.username : (r.username || ''),
|
|
401
|
-
itemNames: (
|
|
392
|
+
itemNames: arrayifyNames(r),
|
|
402
393
|
dateRange: `Du ${formatFR(r.start)} au ${formatFR(r.end)}`,
|
|
403
394
|
adminUrl,
|
|
404
395
|
reason: 'Paiement non reçu dans le temps imparti.',
|
|
@@ -443,13 +434,14 @@ function start() {
|
|
|
443
434
|
// eslint-disable-next-line no-console
|
|
444
435
|
console.info('[calendar-onekite] Scheduler enabled');
|
|
445
436
|
timer = setInterval(() => {
|
|
446
|
-
|
|
447
|
-
|
|
448
|
-
|
|
449
|
-
|
|
450
|
-
|
|
437
|
+
(async () => {
|
|
438
|
+
const locked = await acquireSchedulerLock(55 * 1000);
|
|
439
|
+
if (!locked) return;
|
|
440
|
+
await expirePending();
|
|
441
|
+
await processAwaitingPayment();
|
|
442
|
+
})().catch((err) => {
|
|
451
443
|
// eslint-disable-next-line no-console
|
|
452
|
-
console.warn('[calendar-onekite] Scheduler error
|
|
444
|
+
console.warn('[calendar-onekite] Scheduler tick error', err && err.message ? err.message : err);
|
|
453
445
|
});
|
|
454
446
|
}, 60 * 1000);
|
|
455
447
|
}
|
package/lib/utils.js
ADDED
|
@@ -0,0 +1,70 @@
|
|
|
1
|
+
'use strict';
|
|
2
|
+
|
|
3
|
+
const nconf = require.main.require('nconf');
|
|
4
|
+
|
|
5
|
+
function getSetting(settings, key, fallback) {
|
|
6
|
+
const v = settings && Object.prototype.hasOwnProperty.call(settings, key) ? settings[key] : undefined;
|
|
7
|
+
if (v == null || v === '') return fallback;
|
|
8
|
+
if (typeof v === 'object' && v && typeof v.value !== 'undefined') return v.value;
|
|
9
|
+
return v;
|
|
10
|
+
}
|
|
11
|
+
|
|
12
|
+
function formatFR(tsOrIso) {
|
|
13
|
+
const ts = (typeof tsOrIso === 'string' && tsOrIso.includes('-')) ? Date.parse(tsOrIso) : Number(tsOrIso);
|
|
14
|
+
const d = new Date(Number.isFinite(ts) ? ts : tsOrIso);
|
|
15
|
+
const dd = String(d.getDate()).padStart(2, '0');
|
|
16
|
+
const mm = String(d.getMonth() + 1).padStart(2, '0');
|
|
17
|
+
const yyyy = d.getFullYear();
|
|
18
|
+
return `${dd}/${mm}/${yyyy}`;
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
function formatFRShort(tsOrIso) {
|
|
22
|
+
const ts = (typeof tsOrIso === 'string' && tsOrIso.includes('-')) ? Date.parse(tsOrIso) : Number(tsOrIso);
|
|
23
|
+
const d = new Date(Number.isFinite(ts) ? ts : tsOrIso);
|
|
24
|
+
const dd = String(d.getDate()).padStart(2, '0');
|
|
25
|
+
const mm = String(d.getMonth() + 1).padStart(2, '0');
|
|
26
|
+
return `${dd}/${mm}`;
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
function forumBaseUrl() {
|
|
30
|
+
return String(nconf.get('url') || '').trim().replace(/\/$/, '');
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
function normalizeAllowedGroups(raw) {
|
|
34
|
+
if (!raw) return [];
|
|
35
|
+
if (Array.isArray(raw)) {
|
|
36
|
+
return raw.map((s) => String(s || '').trim()).filter(Boolean);
|
|
37
|
+
}
|
|
38
|
+
return String(raw)
|
|
39
|
+
.split(',')
|
|
40
|
+
.map((s) => String(s || '').trim())
|
|
41
|
+
.filter(Boolean);
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
function normalizeUids(members) {
|
|
45
|
+
if (!Array.isArray(members)) return [];
|
|
46
|
+
const out = [];
|
|
47
|
+
for (const m of members) {
|
|
48
|
+
if (Number.isInteger(m)) { out.push(m); continue; }
|
|
49
|
+
if (typeof m === 'string' && m.trim() && !Number.isNaN(parseInt(m, 10))) { out.push(parseInt(m, 10)); continue; }
|
|
50
|
+
if (m && typeof m === 'object' && (Number.isInteger(m.uid) || (typeof m.uid === 'string' && m.uid.trim()))) {
|
|
51
|
+
const u = Number.isInteger(m.uid) ? m.uid : parseInt(m.uid, 10);
|
|
52
|
+
if (!Number.isNaN(u)) out.push(u);
|
|
53
|
+
}
|
|
54
|
+
}
|
|
55
|
+
return Array.from(new Set(out)).filter((u) => Number.isInteger(u) && u > 0);
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
function arrayifyNames(obj) {
|
|
59
|
+
return (Array.isArray(obj && obj.itemNames) ? obj.itemNames : (obj && obj.itemName ? [obj.itemName] : [])).filter(Boolean);
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
module.exports = {
|
|
63
|
+
getSetting,
|
|
64
|
+
formatFR,
|
|
65
|
+
formatFRShort,
|
|
66
|
+
forumBaseUrl,
|
|
67
|
+
normalizeAllowedGroups,
|
|
68
|
+
normalizeUids,
|
|
69
|
+
arrayifyNames,
|
|
70
|
+
};
|
package/lib/widgets.js
CHANGED
|
@@ -376,13 +376,20 @@ dateClick: function() { window.location.href = calUrl; },
|
|
|
376
376
|
let tRefetch = null;
|
|
377
377
|
const refetch = function () {
|
|
378
378
|
try {
|
|
379
|
+
// Skip work while tab is hidden; refetch once when visible again.
|
|
380
|
+
try {
|
|
381
|
+
if (typeof document !== 'undefined' && document && document.hidden) {
|
|
382
|
+
window.__oneKiteWidgetNeedsRefetch = true;
|
|
383
|
+
return;
|
|
384
|
+
}
|
|
385
|
+
} catch (e) {}
|
|
379
386
|
if (tRefetch) return;
|
|
380
387
|
tRefetch = setTimeout(() => {
|
|
381
388
|
tRefetch = null;
|
|
382
389
|
try {
|
|
383
390
|
calendar.refetchEvents();
|
|
384
391
|
} catch (e) {}
|
|
385
|
-
},
|
|
392
|
+
}, 450);
|
|
386
393
|
} catch (e) {}
|
|
387
394
|
};
|
|
388
395
|
|
|
@@ -402,6 +409,21 @@ dateClick: function() { window.location.href = calUrl; },
|
|
|
402
409
|
};
|
|
403
410
|
socket.on('event:calendar-onekite.calendarUpdated', triggerAll);
|
|
404
411
|
socket.on('event:calendar-onekite.reservationUpdated', triggerAll);
|
|
412
|
+
|
|
413
|
+
// If updates occurred while hidden, refetch once when visible again.
|
|
414
|
+
try {
|
|
415
|
+
if (!window.__oneKiteWidgetVisibilityBound && typeof document !== 'undefined' && document && typeof document.addEventListener === 'function') {
|
|
416
|
+
window.__oneKiteWidgetVisibilityBound = true;
|
|
417
|
+
document.addEventListener('visibilitychange', () => {
|
|
418
|
+
try {
|
|
419
|
+
if (!document.hidden && window.__oneKiteWidgetNeedsRefetch) {
|
|
420
|
+
window.__oneKiteWidgetNeedsRefetch = false;
|
|
421
|
+
triggerAll();
|
|
422
|
+
}
|
|
423
|
+
} catch (e) {}
|
|
424
|
+
}, { passive: true });
|
|
425
|
+
}
|
|
426
|
+
} catch (e) {}
|
|
405
427
|
}
|
|
406
428
|
} catch (e) {}
|
|
407
429
|
|
package/package.json
CHANGED
|
@@ -533,13 +533,40 @@ define('forum/calendar-onekite', ['alerts', 'bootbox', 'hooks'], function (alert
|
|
|
533
533
|
function scheduleRefetch(cal) {
|
|
534
534
|
try {
|
|
535
535
|
if (!cal || typeof cal.refetchEvents !== 'function') return;
|
|
536
|
+
|
|
537
|
+
// Avoid refetching while the tab is hidden; do it once when visible again.
|
|
538
|
+
try {
|
|
539
|
+
if (typeof document !== 'undefined' && document && document.hidden) {
|
|
540
|
+
window.__onekiteNeedsRefetch = true;
|
|
541
|
+
return;
|
|
542
|
+
}
|
|
543
|
+
} catch (e) {}
|
|
544
|
+
|
|
536
545
|
clearTimeout(window.__onekiteRefetchTimer);
|
|
537
546
|
window.__onekiteRefetchTimer = setTimeout(() => {
|
|
538
547
|
try { cal.refetchEvents(); } catch (e) {}
|
|
539
|
-
},
|
|
548
|
+
}, 450);
|
|
540
549
|
} catch (e) {}
|
|
541
550
|
}
|
|
542
551
|
|
|
552
|
+
// If updates occurred while hidden, refetch once when visible again.
|
|
553
|
+
try {
|
|
554
|
+
if (!window.__onekiteVisibilityBound && typeof document !== 'undefined' && document && typeof document.addEventListener === 'function') {
|
|
555
|
+
window.__onekiteVisibilityBound = true;
|
|
556
|
+
document.addEventListener('visibilitychange', () => {
|
|
557
|
+
try {
|
|
558
|
+
if (!document.hidden && window.__onekiteNeedsRefetch) {
|
|
559
|
+
window.__onekiteNeedsRefetch = false;
|
|
560
|
+
const cal = window.oneKiteCalendar;
|
|
561
|
+
if (cal && typeof cal.refetchEvents === 'function') {
|
|
562
|
+
scheduleRefetch(cal);
|
|
563
|
+
}
|
|
564
|
+
}
|
|
565
|
+
} catch (e) {}
|
|
566
|
+
}, { passive: true });
|
|
567
|
+
}
|
|
568
|
+
} catch (e) {}
|
|
569
|
+
|
|
543
570
|
async function fetchJsonCached(url, opts) {
|
|
544
571
|
const cached = jsonCache.get(url);
|
|
545
572
|
const headers = Object.assign({}, (opts && opts.headers) || {});
|
package/plugin.json
CHANGED
package/public/client.js
CHANGED
|
@@ -543,13 +543,40 @@ define('forum/calendar-onekite', ['alerts', 'bootbox', 'hooks'], function (alert
|
|
|
543
543
|
function scheduleRefetch(cal) {
|
|
544
544
|
try {
|
|
545
545
|
if (!cal || typeof cal.refetchEvents !== 'function') return;
|
|
546
|
+
|
|
547
|
+
// Avoid refetching while the tab is hidden; do it once when visible again.
|
|
548
|
+
try {
|
|
549
|
+
if (typeof document !== 'undefined' && document && document.hidden) {
|
|
550
|
+
window.__onekiteNeedsRefetch = true;
|
|
551
|
+
return;
|
|
552
|
+
}
|
|
553
|
+
} catch (e) {}
|
|
554
|
+
|
|
546
555
|
clearTimeout(window.__onekiteRefetchTimer);
|
|
547
556
|
window.__onekiteRefetchTimer = setTimeout(() => {
|
|
548
557
|
try { cal.refetchEvents(); } catch (e) {}
|
|
549
|
-
},
|
|
558
|
+
}, 450);
|
|
550
559
|
} catch (e) {}
|
|
551
560
|
}
|
|
552
561
|
|
|
562
|
+
// If updates occurred while hidden, refetch once when visible again.
|
|
563
|
+
try {
|
|
564
|
+
if (!window.__onekiteVisibilityBound && typeof document !== 'undefined' && document && typeof document.addEventListener === 'function') {
|
|
565
|
+
window.__onekiteVisibilityBound = true;
|
|
566
|
+
document.addEventListener('visibilitychange', () => {
|
|
567
|
+
try {
|
|
568
|
+
if (!document.hidden && window.__onekiteNeedsRefetch) {
|
|
569
|
+
window.__onekiteNeedsRefetch = false;
|
|
570
|
+
const cal = window.oneKiteCalendar;
|
|
571
|
+
if (cal && typeof cal.refetchEvents === 'function') {
|
|
572
|
+
scheduleRefetch(cal);
|
|
573
|
+
}
|
|
574
|
+
}
|
|
575
|
+
} catch (e) {}
|
|
576
|
+
}, { passive: true });
|
|
577
|
+
}
|
|
578
|
+
} catch (e) {}
|
|
579
|
+
|
|
553
580
|
async function fetchJsonCached(url, opts) {
|
|
554
581
|
const cached = jsonCache.get(url);
|
|
555
582
|
const headers = Object.assign({}, (opts && opts.headers) || {});
|