nodebb-plugin-ezoic-infinite 1.7.45 → 1.7.47
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/library.js +29 -13
- package/package.json +1 -1
- package/public/client.js +71 -101
package/library.js
CHANGED
|
@@ -15,9 +15,11 @@ function normalizeExcludedGroups(value) {
|
|
|
15
15
|
// NodeBB stocke les settings multi-valeurs comme string JSON "[\"group1\",\"group2\"]"
|
|
16
16
|
const s = String(value).trim();
|
|
17
17
|
if (s.startsWith('[')) {
|
|
18
|
-
try {
|
|
18
|
+
try {
|
|
19
|
+
const parsed = JSON.parse(s);
|
|
20
|
+
if (Array.isArray(parsed)) return parsed.map(String).filter(Boolean);
|
|
21
|
+
} catch (_) {}
|
|
19
22
|
}
|
|
20
|
-
// Fallback : séparation par virgule
|
|
21
23
|
return s.split(',').map(v => v.trim()).filter(Boolean);
|
|
22
24
|
}
|
|
23
25
|
|
|
@@ -28,7 +30,15 @@ function parseBool(v, def = false) {
|
|
|
28
30
|
return s === '1' || s === 'true' || s === 'on' || s === 'yes';
|
|
29
31
|
}
|
|
30
32
|
|
|
33
|
+
// ── Cache groupes (fix #7 : getAllGroups sans cache sur page ACP) ───────────
|
|
34
|
+
|
|
35
|
+
let _groupsCache = null;
|
|
36
|
+
let _groupsCacheAt = 0;
|
|
37
|
+
const GROUPS_TTL = 60_000; // 1 minute
|
|
38
|
+
|
|
31
39
|
async function getAllGroups() {
|
|
40
|
+
const now = Date.now();
|
|
41
|
+
if (_groupsCache && (now - _groupsCacheAt) < GROUPS_TTL) return _groupsCache;
|
|
32
42
|
let names = await db.getSortedSetRange('groups:createtime', 0, -1);
|
|
33
43
|
if (!names || !names.length) {
|
|
34
44
|
names = await db.getSortedSetRange('groups:visible:createtime', 0, -1);
|
|
@@ -37,7 +47,9 @@ async function getAllGroups() {
|
|
|
37
47
|
const data = await groups.getGroupsData(filtered);
|
|
38
48
|
const valid = data.filter(g => g && g.name);
|
|
39
49
|
valid.sort((a, b) => String(a.name).localeCompare(String(b.name), undefined, { sensitivity: 'base' }));
|
|
40
|
-
|
|
50
|
+
_groupsCache = valid;
|
|
51
|
+
_groupsCacheAt = now;
|
|
52
|
+
return _groupsCache;
|
|
41
53
|
}
|
|
42
54
|
|
|
43
55
|
// ── Settings cache (30s TTL) ────────────────────────────────────────────────
|
|
@@ -88,7 +100,10 @@ ezstandalone.cmd = ezstandalone.cmd || [];
|
|
|
88
100
|
// ── Hooks ──────────────────────────────────────────────────────────────────
|
|
89
101
|
|
|
90
102
|
plugin.onSettingsSet = function (data) {
|
|
91
|
-
if (data && data.hash === SETTINGS_KEY)
|
|
103
|
+
if (data && data.hash === SETTINGS_KEY) {
|
|
104
|
+
_settingsCache = null;
|
|
105
|
+
_groupsCache = null; // invalider aussi le cache groupes
|
|
106
|
+
}
|
|
92
107
|
};
|
|
93
108
|
|
|
94
109
|
plugin.addAdminNavigation = async (header) => {
|
|
@@ -101,11 +116,11 @@ plugin.addAdminNavigation = async (header) => {
|
|
|
101
116
|
* Injecte les scripts Ezoic dans le <head> via templateData.customHTML.
|
|
102
117
|
*
|
|
103
118
|
* NodeBB v4 / thème Harmony : header.tpl contient {{customHTML}} dans le <head>
|
|
104
|
-
* (render.js
|
|
105
|
-
* Le hook filter:middleware.renderHeader reçoit templateData = headerFooterData
|
|
106
|
-
*
|
|
107
|
-
*
|
|
108
|
-
*
|
|
119
|
+
* (render.js : templateValues.customHTML = meta.config.customHTML).
|
|
120
|
+
* Le hook filter:middleware.renderHeader reçoit templateData = headerFooterData.
|
|
121
|
+
* On préfixe customHTML pour passer AVANT le customHTML admin tout en le préservant.
|
|
122
|
+
*
|
|
123
|
+
* Fix #3 : erreurs loggées côté serveur plutôt qu'avalées silencieusement.
|
|
109
124
|
*/
|
|
110
125
|
plugin.injectEzoicHead = async (data) => {
|
|
111
126
|
try {
|
|
@@ -113,17 +128,18 @@ plugin.injectEzoicHead = async (data) => {
|
|
|
113
128
|
const uid = data.req?.uid ?? 0;
|
|
114
129
|
const excluded = await isUserExcluded(uid, settings.excludedGroups);
|
|
115
130
|
if (!excluded) {
|
|
116
|
-
// Préfixer : nos scripts d'abord, puis le customHTML existant de l'admin
|
|
117
131
|
data.templateData.customHTML = EZOIC_SCRIPTS + (data.templateData.customHTML || '');
|
|
118
132
|
}
|
|
119
|
-
} catch (
|
|
133
|
+
} catch (err) {
|
|
134
|
+
// Log l'erreur mais ne pas planter le rendu de la page
|
|
135
|
+
console.error('[ezoic-infinite] injectEzoicHead error:', err.message || err);
|
|
136
|
+
}
|
|
120
137
|
return data;
|
|
121
138
|
};
|
|
122
139
|
|
|
123
140
|
plugin.init = async ({ router, middleware }) => {
|
|
124
141
|
async function render(req, res) {
|
|
125
|
-
const settings
|
|
126
|
-
const allGroups = await getAllGroups();
|
|
142
|
+
const [settings, allGroups] = await Promise.all([getSettings(), getAllGroups()]);
|
|
127
143
|
res.render('admin/plugins/ezoic-infinite', {
|
|
128
144
|
title: 'Ezoic Infinite Ads',
|
|
129
145
|
...settings,
|
package/package.json
CHANGED
package/public/client.js
CHANGED
|
@@ -1,69 +1,30 @@
|
|
|
1
1
|
/**
|
|
2
|
-
* NodeBB Ezoic Infinite Ads — client.js
|
|
2
|
+
* NodeBB Ezoic Infinite Ads — client.js v51
|
|
3
3
|
*
|
|
4
4
|
* Historique des corrections majeures
|
|
5
5
|
* ────────────────────────────────────
|
|
6
6
|
* v18 Ancrage stable par data-pid / data-index au lieu d'ordinalMap fragile.
|
|
7
|
-
*
|
|
8
|
-
* v19 Intervalle global basé sur l'ordinal absolu (data-index), pas sur
|
|
9
|
-
* la position dans le batch courant.
|
|
10
|
-
*
|
|
7
|
+
* v19 Intervalle global basé sur l'ordinal absolu (data-index).
|
|
11
8
|
* v20 Table KIND : anchorAttr / ordinalAttr / baseTag par kindClass.
|
|
12
|
-
*
|
|
13
|
-
*
|
|
14
|
-
*
|
|
15
|
-
*
|
|
16
|
-
* v25 Fix scroll-up / virtualisation NodeBB : decluster + grace period.
|
|
17
|
-
*
|
|
18
|
-
* v26 Suppression définitive du recyclage d'id (causait réinjection en haut).
|
|
19
|
-
*
|
|
20
|
-
* v27 pruneOrphans supprimé (faux-orphelins sur virtualisation NodeBB posts).
|
|
21
|
-
*
|
|
9
|
+
* IO fixe (une instance, jamais recréée). Fix TCF locator.
|
|
10
|
+
* v25 Fix scroll-up / virtualisation NodeBB.
|
|
11
|
+
* v26 Suppression recyclage d'id (causait réinjection en haut).
|
|
12
|
+
* v27 pruneOrphans supprimé (faux-orphelins sur virtualisation posts).
|
|
22
13
|
* v28 decluster supprimé. Wraps persistants pendant la session.
|
|
23
|
-
*
|
|
24
|
-
*
|
|
25
|
-
*
|
|
26
|
-
*
|
|
27
|
-
*
|
|
28
|
-
*
|
|
29
|
-
*
|
|
30
|
-
*
|
|
31
|
-
*
|
|
32
|
-
*
|
|
33
|
-
*
|
|
34
|
-
*
|
|
35
|
-
*
|
|
36
|
-
*
|
|
37
|
-
* rechargement. Redondant depuis le fix normalizeExcludedGroups (v49).
|
|
38
|
-
*
|
|
39
|
-
* v43 Seuil de recyclage abaissé à -vh + unobserve avant recyclage.
|
|
40
|
-
*
|
|
41
|
-
* v42 Seuil -(IO_MARGIN + vh) (trop strict, peu de wraps éligibles).
|
|
42
|
-
*
|
|
43
|
-
* v41 Seuil -1vh (trop permissif sur mobile, ignorait IO_MARGIN).
|
|
44
|
-
*
|
|
45
|
-
* v40 Recyclage slots via destroyPlaceholders+define+displayMore avec délais.
|
|
46
|
-
* Séquence : destroy → 300ms → define → 300ms → displayMore.
|
|
47
|
-
* Testé manuellement : fonctionne. displayMore = API Ezoic infinite scroll.
|
|
48
|
-
*
|
|
49
|
-
* v38 Pool épuisé = fin de quota Ezoic par page-view. ez.refresh() interdit
|
|
50
|
-
* sur la même page que ez.enable() — supprimé. moveDistantWrap supprimé :
|
|
51
|
-
* déplacer un wrap "already defined" ne re-sert aucune pub. Pool épuisé →
|
|
52
|
-
* break propre dans injectBetween. muteConsole : ajout warnings refresh.
|
|
53
|
-
*
|
|
54
|
-
* v36 Optimisations chemin critique (scroll → injectBetween) :
|
|
55
|
-
* – S.wrapByKey Map<anchorKey,wrap> : findWrap() passe de querySelector
|
|
56
|
-
* sur tout le doc à un lookup O(1). Mis à jour dans insertAfter,
|
|
57
|
-
* dropWrap et cleanup.
|
|
58
|
-
* – wrapIsLive allégé : pour les voisins immédiats on vérifie les
|
|
59
|
-
* attributs du nœud lui-même sans querySelector global.
|
|
60
|
-
* – MutationObserver : matches() vérifié avant querySelector() pour
|
|
61
|
-
* court-circuiter les sous-arbres entiers ajoutés par NodeBB.
|
|
62
|
-
*
|
|
63
|
-
* v35 Revue complète prod-ready :
|
|
64
|
-
* – initPools protégé contre ré-initialisation inutile (S.poolsReady).
|
|
65
|
-
* – muteConsole élargit à "No valid placeholders for loadMore".
|
|
66
|
-
* – Commentaires et historique nettoyés.
|
|
14
|
+
* v32 pruneOrphansBetween réactivé uniquement pour topics de catégorie.
|
|
15
|
+
* NodeBB NE virtualise PAS les topics → prune safe.
|
|
16
|
+
* Toujours désactivé pour posts (virtualisation → faux-orphelins).
|
|
17
|
+
* v35 initPools protégé (S.poolsReady). muteConsole élargi.
|
|
18
|
+
* v36 S.wrapByKey Map O(1). wrapIsLive allégé. MutationObserver optimisé.
|
|
19
|
+
* v38 ez.refresh() interdit → supprimé. Pool épuisé → break propre.
|
|
20
|
+
* v40 Recyclage via destroyPlaceholders+define+displayMore (délais 300ms).
|
|
21
|
+
* v43 Seuil recyclage -1vh + unobserve avant déplacement.
|
|
22
|
+
* v49 Fix normalizeExcludedGroups : JSON.parse du tableau NodeBB.
|
|
23
|
+
* v50 bindLoginCheck() supprimé (NodeBB recharge après login).
|
|
24
|
+
* v51 #2 fetchConfig() : backoff 10s sur échec réseau (évite spam API).
|
|
25
|
+
* #4 recycleAndMove : re-observe le placeholder après displayMore.
|
|
26
|
+
* #5 ensureTcfLocator : MutationObserver global disconnecté au cleanup.
|
|
27
|
+
* #6 getIO() recrée l'observer si le type d'écran change (resize).
|
|
67
28
|
*/
|
|
68
29
|
(function nbbEzoicInfinite() {
|
|
69
30
|
'use strict';
|
|
@@ -78,7 +39,6 @@
|
|
|
78
39
|
const A_SHOWN = 'data-ezoic-shown'; // timestamp dernier showAds ms
|
|
79
40
|
|
|
80
41
|
const EMPTY_CHECK_MS = 20_000; // délai avant collapse d'un wrap vide post-show
|
|
81
|
-
const MIN_PRUNE_AGE_MS = 8_000; // délai de grâce avant pruning (stabilisation DOM)
|
|
82
42
|
const MAX_INSERTS_RUN = 6; // max insertions par appel runCore
|
|
83
43
|
const MAX_INFLIGHT = 4; // max showAds() simultanés
|
|
84
44
|
const SHOW_THROTTLE_MS = 900; // anti-spam showAds() par id
|
|
@@ -127,6 +87,8 @@
|
|
|
127
87
|
pending: [], // ids en attente de slot inflight
|
|
128
88
|
pendingSet: new Set(),
|
|
129
89
|
wrapByKey: new Map(), // anchorKey → wrap DOM node
|
|
90
|
+
recycling: new Set(), // ids en cours de séquence destroy→define→displayMore
|
|
91
|
+
tcfObs: null, // MutationObserver TCF locator
|
|
130
92
|
runQueued: false,
|
|
131
93
|
burstActive: false,
|
|
132
94
|
burstDeadline: 0,
|
|
@@ -149,12 +111,17 @@
|
|
|
149
111
|
|
|
150
112
|
// ── Config ─────────────────────────────────────────────────────────────────
|
|
151
113
|
|
|
114
|
+
// Fix #2 : backoff 10s sur échec pour éviter de spammer l'API
|
|
115
|
+
// Fix #3 : _cfgErrorUntil au scope IIFE pour être réinitialisé dans cleanup()
|
|
116
|
+
let _cfgErrorUntil = 0;
|
|
152
117
|
async function fetchConfig() {
|
|
153
118
|
if (S.cfg) return S.cfg;
|
|
119
|
+
if (Date.now() < _cfgErrorUntil) return null;
|
|
154
120
|
try {
|
|
155
121
|
const r = await fetch('/api/plugins/ezoic-infinite/config', { credentials: 'same-origin' });
|
|
156
|
-
if (r.ok) S.cfg = await r.json();
|
|
157
|
-
|
|
122
|
+
if (r.ok) { S.cfg = await r.json(); }
|
|
123
|
+
else { _cfgErrorUntil = Date.now() + 10_000; }
|
|
124
|
+
} catch (_) { _cfgErrorUntil = Date.now() + 10_000; }
|
|
158
125
|
return S.cfg;
|
|
159
126
|
}
|
|
160
127
|
|
|
@@ -291,6 +258,9 @@
|
|
|
291
258
|
function pickId(poolKey) {
|
|
292
259
|
const pool = S.pools[poolKey];
|
|
293
260
|
if (!pool.length) return null;
|
|
261
|
+
// Fix #2 : early-exit si tous les ids sont déjà montés — évite O(n) inutile
|
|
262
|
+
if (S.mountedIds.size >= pool.length &&
|
|
263
|
+
pool.every(id => S.mountedIds.has(id))) return null;
|
|
294
264
|
for (let t = 0; t < pool.length; t++) {
|
|
295
265
|
const i = S.cursors[poolKey] % pool.length;
|
|
296
266
|
S.cursors[poolKey] = (S.cursors[poolKey] + 1) % pool.length;
|
|
@@ -336,6 +306,9 @@
|
|
|
336
306
|
if (!best) return null;
|
|
337
307
|
const id = parseInt(best.getAttribute(A_WRAPID), 10);
|
|
338
308
|
if (!Number.isFinite(id)) return null;
|
|
309
|
+
// Fix #1 : éviter de recycler un slot dont la séquence Ezoic est en cours
|
|
310
|
+
if (S.recycling.has(id)) return null;
|
|
311
|
+
S.recycling.add(id);
|
|
339
312
|
|
|
340
313
|
const oldKey = best.getAttribute(A_ANCHOR);
|
|
341
314
|
// Neutraliser l'IO sur ce wrap avant déplacement — évite un showAds
|
|
@@ -356,7 +329,16 @@
|
|
|
356
329
|
// Délais requis : destroyPlaceholders est asynchrone en interne
|
|
357
330
|
const doDestroy = () => { try { ez.destroyPlaceholders([id]); } catch (_) {} setTimeout(doDefine, 300); };
|
|
358
331
|
const doDefine = () => { try { ez.define([id]); } catch (_) {} setTimeout(doDisplay, 300); };
|
|
359
|
-
|
|
332
|
+
// Fix #4 : re-observer le ph après displayMore pour déclencher scheduleEmptyCheck
|
|
333
|
+
// si la pub ne charge pas (détection wrap vide).
|
|
334
|
+
const doDisplay = () => {
|
|
335
|
+
try { ez.displayMore([id]); } catch (_) {}
|
|
336
|
+
S.recycling.delete(id); // Fix #1 : séquence terminée
|
|
337
|
+
observePh(id);
|
|
338
|
+
const t = ts();
|
|
339
|
+
try { best.setAttribute(A_SHOWN, String(t)); } catch (_) {}
|
|
340
|
+
scheduleEmptyCheck(id, t);
|
|
341
|
+
};
|
|
360
342
|
try { (typeof ez.cmd?.push === 'function') ? ez.cmd.push(doDestroy) : doDestroy(); } catch (_) {}
|
|
361
343
|
|
|
362
344
|
return { id, wrap: best };
|
|
@@ -403,37 +385,6 @@
|
|
|
403
385
|
} catch (_) {}
|
|
404
386
|
}
|
|
405
387
|
|
|
406
|
-
// ── Prune (topics de catégorie uniquement) ────────────────────────────────
|
|
407
|
-
//
|
|
408
|
-
// Réactivé uniquement pour 'ezoic-ad-between' (liste de topics).
|
|
409
|
-
//
|
|
410
|
-
// Safe ici car NodeBB ne virtualise PAS les topics dans une catégorie :
|
|
411
|
-
// les li[component="category/topic"] restent dans le DOM pendant toute
|
|
412
|
-
// la session. Un wrap orphelin (ancre absente) signifie vraiment que le
|
|
413
|
-
// topic a disparu. Sans ce nettoyage, les wraps s'accumulent en tête de
|
|
414
|
-
// liste après un long scroll et bloquent les nouvelles injections.
|
|
415
|
-
//
|
|
416
|
-
// Toujours désactivé pour 'ezoic-ad-message' (posts de topic) :
|
|
417
|
-
// NodeBB virtualise les posts hors-viewport — il les retire puis les
|
|
418
|
-
// réinsère. pruneOrphans verrait des ancres temporairement absentes,
|
|
419
|
-
// supprimerait les wraps, et provoquerait une réinjection en haut.
|
|
420
|
-
|
|
421
|
-
function pruneOrphansBetween() {
|
|
422
|
-
const klass = 'ezoic-ad-between';
|
|
423
|
-
const cfg = KIND[klass];
|
|
424
|
-
|
|
425
|
-
document.querySelectorAll(`.${WRAP_CLASS}.${klass}`).forEach(w => {
|
|
426
|
-
const created = parseInt(w.getAttribute(A_CREATED) || '0', 10);
|
|
427
|
-
if (ts() - created < MIN_PRUNE_AGE_MS) return; // grâce post-création
|
|
428
|
-
|
|
429
|
-
const key = w.getAttribute(A_ANCHOR) ?? '';
|
|
430
|
-
const sid = key.slice(klass.length + 1); // après "ezoic-ad-between:"
|
|
431
|
-
if (!sid) { mutate(() => dropWrap(w)); return; }
|
|
432
|
-
|
|
433
|
-
const anchorEl = document.querySelector(`${cfg.baseTag}[${cfg.anchorAttr}="${sid}"]`);
|
|
434
|
-
if (!anchorEl?.isConnected) mutate(() => dropWrap(w));
|
|
435
|
-
});
|
|
436
|
-
}
|
|
437
388
|
|
|
438
389
|
// ── Injection ──────────────────────────────────────────────────────────────
|
|
439
390
|
|
|
@@ -486,8 +437,15 @@
|
|
|
486
437
|
|
|
487
438
|
// ── IntersectionObserver & Show ────────────────────────────────────────────
|
|
488
439
|
|
|
440
|
+
// Fix #6 : recréer l'observer si le type d'écran change (rotation, resize).
|
|
441
|
+
// isMobile() est évalué à chaque appel pour détecter un changement.
|
|
442
|
+
let _ioMobile = null; // dernier état mobile/desktop pour lequel l'IO a été créé
|
|
489
443
|
function getIO() {
|
|
490
|
-
|
|
444
|
+
const mobile = isMobile();
|
|
445
|
+
if (S.io && _ioMobile === mobile) return S.io;
|
|
446
|
+
// Type d'écran changé ou première création : (re)créer l'observer
|
|
447
|
+
if (S.io) { try { S.io.disconnect(); } catch (_) {} S.io = null; }
|
|
448
|
+
_ioMobile = mobile;
|
|
491
449
|
try {
|
|
492
450
|
S.io = new IntersectionObserver(entries => {
|
|
493
451
|
for (const e of entries) {
|
|
@@ -496,7 +454,7 @@
|
|
|
496
454
|
const id = parseInt(e.target.getAttribute('data-ezoic-id'), 10);
|
|
497
455
|
if (Number.isFinite(id) && id > 0) enqueueShow(id);
|
|
498
456
|
}
|
|
499
|
-
}, { root: null, rootMargin:
|
|
457
|
+
}, { root: null, rootMargin: mobile ? IO_MARGIN_MOBILE : IO_MARGIN_DESKTOP, threshold: 0 });
|
|
500
458
|
} catch (_) { S.io = null; }
|
|
501
459
|
return S.io;
|
|
502
460
|
}
|
|
@@ -633,7 +591,6 @@
|
|
|
633
591
|
);
|
|
634
592
|
|
|
635
593
|
if (kind === 'categoryTopics') {
|
|
636
|
-
pruneOrphansBetween();
|
|
637
594
|
return exec(
|
|
638
595
|
'ezoic-ad-between', getTopics,
|
|
639
596
|
cfg.enableBetweenAds, cfg.intervalPosts, cfg.showFirstTopicAd, 'topics'
|
|
@@ -691,6 +648,7 @@
|
|
|
691
648
|
blockedUntil = ts() + 1500;
|
|
692
649
|
mutate(() => document.querySelectorAll(`.${WRAP_CLASS}`).forEach(dropWrap));
|
|
693
650
|
S.cfg = null;
|
|
651
|
+
_cfgErrorUntil = 0; // Fix #3 : réinitialiser le backoff entre les pages
|
|
694
652
|
S.poolsReady = false;
|
|
695
653
|
S.pools = { topics: [], posts: [], categories: [] };
|
|
696
654
|
S.cursors = { topics: 0, posts: 0, categories: 0 };
|
|
@@ -702,6 +660,9 @@
|
|
|
702
660
|
S.pendingSet.clear();
|
|
703
661
|
S.burstActive = false;
|
|
704
662
|
S.runQueued = false;
|
|
663
|
+
S.recycling.clear();
|
|
664
|
+
if (S.domObs) { S.domObs.disconnect(); S.domObs = null; }
|
|
665
|
+
if (S.tcfObs) { S.tcfObs.disconnect(); S.tcfObs = null; }
|
|
705
666
|
}
|
|
706
667
|
|
|
707
668
|
// ── MutationObserver ───────────────────────────────────────────────────────
|
|
@@ -758,9 +719,10 @@
|
|
|
758
719
|
(document.body || document.documentElement).appendChild(f);
|
|
759
720
|
};
|
|
760
721
|
inject();
|
|
761
|
-
|
|
762
|
-
|
|
763
|
-
|
|
722
|
+
// Fix #5 : ref stockée dans S pour pouvoir déconnecter au cleanup
|
|
723
|
+
if (!S.tcfObs) {
|
|
724
|
+
S.tcfObs = new MutationObserver(inject);
|
|
725
|
+
S.tcfObs.observe(document.documentElement, { childList: true, subtree: true });
|
|
764
726
|
}
|
|
765
727
|
} catch (_) {}
|
|
766
728
|
}
|
|
@@ -798,7 +760,9 @@
|
|
|
798
760
|
$(window).on('action:ajaxify.end.nbbEzoic', () => {
|
|
799
761
|
S.pageKey = pageKey();
|
|
800
762
|
blockedUntil = 0;
|
|
801
|
-
muteConsole
|
|
763
|
+
// Fix #4 : muteConsole/ensureTcfLocator/warmNetwork déjà appelés au boot
|
|
764
|
+
// et sont idempotents — pas besoin de les rappeler à chaque navigation.
|
|
765
|
+
muteConsole();
|
|
802
766
|
patchShowAds(); getIO(); ensureDomObserver(); requestBurst();
|
|
803
767
|
});
|
|
804
768
|
|
|
@@ -826,6 +790,12 @@
|
|
|
826
790
|
ticking = true;
|
|
827
791
|
requestAnimationFrame(() => { ticking = false; requestBurst(); });
|
|
828
792
|
}, { passive: true });
|
|
793
|
+
// Fix #6 : détecter rotation/resize pour recréer l'IO avec la bonne marge
|
|
794
|
+
let resizeTimer = 0;
|
|
795
|
+
window.addEventListener('resize', () => {
|
|
796
|
+
clearTimeout(resizeTimer);
|
|
797
|
+
resizeTimer = setTimeout(() => { getIO(); }, 500);
|
|
798
|
+
}, { passive: true });
|
|
829
799
|
}
|
|
830
800
|
|
|
831
801
|
// ── Boot ───────────────────────────────────────────────────────────────────
|