discovery-media-player 0.1.78 → 0.1.80
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/docs/HOST-CONTRACT.md +7 -0
- package/package.json +1 -1
- package/server/handler.js +6 -0
- package/server/retention.js +114 -34
- package/server/routes-liens.js +13 -4
- package/supabase/recensement-retention.sql +17 -8
package/docs/HOST-CONTRACT.md
CHANGED
|
@@ -35,6 +35,7 @@ need.
|
|
|
35
35
|
"frameAncestors": ["'self'", "https://*.vercel.app", "https://app.example.com"],
|
|
36
36
|
"separateIssuer": true,
|
|
37
37
|
"internalStrict": true,
|
|
38
|
+
"retentionSweep": false,
|
|
38
39
|
"hostShare": true,
|
|
39
40
|
"hostMail": true,
|
|
40
41
|
"plugins": { "bot": false, "visitors": false, "brandIntro": false, "botBrowser": false, "providerQuotas": false },
|
|
@@ -95,6 +96,12 @@ exactly the monitoring case where "no data" would otherwise read as "all clear".
|
|
|
95
96
|
⚠️ **`incomplet` wins over `partiel`**: a missing column is a positive fact and settles the verdict
|
|
96
97
|
on its own, even when the rest has not been checked.
|
|
97
98
|
|
|
99
|
+
⚠️ **`retentionSweep`** says whether the automatic retention purge is *armed*
|
|
100
|
+
(`config.retention.balayage === true`). The `retention` capability only says the instance *can*
|
|
101
|
+
purge; this boolean says whether it *does*, on its own, once a day. Default `false` — nothing is
|
|
102
|
+
deleted unless an operator wrote the policy. A cockpit can read this to know if an instance is
|
|
103
|
+
subject to automatic deletion, instead of inferring it from a log.
|
|
104
|
+
|
|
98
105
|
⚠️ **`internalStrict: false` means internal identity comes from the browser.** In transitional
|
|
99
106
|
mode the internal-analytics route accepts `docId`, `email` and `name` as the client declares them —
|
|
100
107
|
a caller can fabricate "this colleague read this document". Strict mode (`PLAYER_INTERNAL_STRICT=1`)
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "discovery-media-player",
|
|
3
|
-
"version": "0.1.
|
|
3
|
+
"version": "0.1.80",
|
|
4
4
|
"description": "Self-hosted document viewer: per-recipient tracked links, reading analytics, live presentation. The core knows nothing about the application hosting it — everything it borrows arrives through an injected context.",
|
|
5
5
|
"keywords": [
|
|
6
6
|
"pdf-viewer",
|
package/server/handler.js
CHANGED
|
@@ -595,6 +595,12 @@ async function handler(req, res) {
|
|
|
595
595
|
// MESURABLE : un cockpit peut refuser une instance non stricte, au lieu de le découvrir en
|
|
596
596
|
// lisant un journal. Demandé par le second hôte (cinquième audit, P1-4).
|
|
597
597
|
internalStrict: !!(PLAYER.config && PLAYER.config.internalStrict),
|
|
598
|
+
// ⚠️ LE BALAYAGE DE RÉTENTION EST-IL ARMÉ ? La capacité `retention` dit que l'instance PEUT
|
|
599
|
+
// purger ; ce booléen dit si le balayage automatique TOURNE (`config.retention.balayage`).
|
|
600
|
+
// Sans lui, une instance armée est indiscernable d'une instance éteinte — et une purge qui
|
|
601
|
+
// supprime se compose mal avec l'ignorance de savoir si l'on est concerné (signalé par le
|
|
602
|
+
// second hôte). Défaut false, comme la décision par défaut : rien ne s'efface tout seul.
|
|
603
|
+
retentionSweep: !!(PLAYER.config && PLAYER.config.retention && PLAYER.config.retention.balayage === true),
|
|
598
604
|
// ⚠️ L'ÉTAT DU SCHÉMA, LÀ OÙ ON REGARDE. Une colonne absente était signalée par un
|
|
599
605
|
// `console.warn`, une fois par processus : sur une fonction serverless, une ligne perdue
|
|
600
606
|
// dans une sortie que personne n'ouvre tant que tout a l'air de marcher — et « tout a
|
package/server/retention.js
CHANGED
|
@@ -57,52 +57,134 @@ function borne(now, mois) {
|
|
|
57
57
|
// Bornes d'exécution par défaut — un lot raisonnable, un plafond qui tient dans une fenêtre
|
|
58
58
|
// serverless. L'appelant peut resserrer (dryRun, taille, plafond) ; jamais dépasser sans le dire.
|
|
59
59
|
const LOT = 200, PLAFOND = 5000, PLAFOND_PRESENTATIONS = 500;
|
|
60
|
+
const MAX_TAILLE = 500, MAX_PLAFOND = 5000;
|
|
61
|
+
|
|
62
|
+
// ⚠️ LES OPTIONS D'EXÉCUTION SONT VALIDÉES AVANT TOUT DELETE (P1 neuvième audit). La route les
|
|
63
|
+
// reçoit d'un appelant ; un `dryRun` non booléen strict, une taille/plafond hors bornes ou une
|
|
64
|
+
// clé inconnue font ÉCHOUER la purge — jamais un `Number()` ni un `!!` qui transformerait
|
|
65
|
+
// `"false"` en purge réelle. `dryRun` est ce qu'un exploitant lance EN PREMIER : il ne doit pas
|
|
66
|
+
// pouvoir supprimer par une faute de type.
|
|
67
|
+
const CLES_OPTS = new Set(["dryRun", "taille", "plafond"]);
|
|
68
|
+
function optionsValidees(opts) {
|
|
69
|
+
const o = opts || {};
|
|
70
|
+
for (const cle of Object.keys(o)) {
|
|
71
|
+
if (!CLES_OPTS.has(cle)) { const e = new Error(`option de rétention inconnue : ${cle}`); e.retentionInvalide = true; throw e; }
|
|
72
|
+
}
|
|
73
|
+
if ("dryRun" in o && typeof o.dryRun !== "boolean") { const e = new Error("dryRun doit être un booléen strict"); e.retentionInvalide = true; throw e; }
|
|
74
|
+
const entierDans = (v, min, max, nom) => {
|
|
75
|
+
if (v === undefined) return undefined;
|
|
76
|
+
if (typeof v !== "number" || !Number.isInteger(v) || v < min || v > max) {
|
|
77
|
+
const e = new Error(`${nom} doit être un entier dans [${min},${max}] — reçu ${JSON.stringify(v)}`); e.retentionInvalide = true; throw e;
|
|
78
|
+
}
|
|
79
|
+
return v;
|
|
80
|
+
};
|
|
81
|
+
return {
|
|
82
|
+
dryRun: o.dryRun === true,
|
|
83
|
+
taille: entierDans(o.taille, 1, MAX_TAILLE, "taille") ?? LOT,
|
|
84
|
+
plafond: entierDans(o.plafond, 1, MAX_PLAFOND, "plafond") ?? PLAFOND,
|
|
85
|
+
};
|
|
86
|
+
}
|
|
60
87
|
|
|
61
88
|
// Une valeur pour `id=in.(…)` : double-guillemets, guillemet et antislash internes échappés —
|
|
62
89
|
// PostgREST exige le guillemetage dès qu'une valeur porte un caractère réservé (`:` d'une clé de
|
|
63
90
|
// débit, par exemple). Guillemeter TOUJOURS est correct et évite d'avoir à deviner.
|
|
64
91
|
const guill = (v) => '"' + String(v).replace(/\\/g, "\\\\").replace(/"/g, '\\"') + '"';
|
|
65
92
|
|
|
93
|
+
// ⚠️ LES DEUX SEULES PORTES DE DESTRUCTION DU MODULE — dryRun court-circuité en PREMIÈRE ligne.
|
|
94
|
+
// « Honoré » ne se prouve pas en lisant chaque DELETE (une fenêtre de lecture décide de la réponse
|
|
95
|
+
// — le piège du périmètre), mais en n'ayant QU'UNE porte, gardée ici, et une garde de forme qui
|
|
96
|
+
// exige qu'il n'y en ait qu'une (server/__tests__/retentionUnePorte.test.js). Un troisième chemin
|
|
97
|
+
// d'écriture DEVRA passer par ces portes, ou il rougira le compte. C'est « retirer la seconde
|
|
98
|
+
// source de vérité » appliqué à la suppression : un seul endroit peut détruire.
|
|
99
|
+
async function effacerParIds(table, colId, ids, opts) {
|
|
100
|
+
if (opts.dryRun || !ids || !ids.length) return 0;
|
|
101
|
+
const del = await PLAYER.db.request(`${table}?${colId}=in.(${ids.map(guill).join(",")})&select=${colId}`, { method: "DELETE", headers: { Prefer: "return=representation" } });
|
|
102
|
+
return Array.isArray(del) ? del.length : 0; // lignes RENDUES, pas ids présélectionnés
|
|
103
|
+
}
|
|
104
|
+
async function retirerFichier(chemin, opts) {
|
|
105
|
+
if (opts.dryRun || !chemin) return null; // null = rien tenté ; true = retiré ; false = échec
|
|
106
|
+
const retirer = PLAYER.storage && typeof PLAYER.storage.remove === "function" ? PLAYER.storage.remove.bind(PLAYER.storage) : null;
|
|
107
|
+
if (!retirer) return null;
|
|
108
|
+
try { return !!(await retirer("present-attachments", chemin)); } catch { return false; }
|
|
109
|
+
}
|
|
110
|
+
|
|
66
111
|
// ⚠️ PURGE PAR LOTS BORNÉS (P2 huitième audit). On SÉLECTIONNE un lot d'identifiants (borné,
|
|
67
112
|
// ordonné par la colonne de date), on les supprime par `id=in.(…)`, on recommence — jamais un
|
|
68
113
|
// DELETE non borné qui ramènerait tout l'historique d'un coup (mémoire, WAL, verrous, timeout).
|
|
69
114
|
// `dryRun` sélectionne sans supprimer. Le rapport remplace la liste d'identifiants : examinées,
|
|
70
115
|
// supprimées, tronqué (il reste à faire au prochain passage).
|
|
116
|
+
// ⚠️ Pagination par CURSEUR KEYSET (`colId=gt.<dernier>`), pas par `offset` — la garde de
|
|
117
|
+
// portabilité de la forge interdit `offset=`, et un curseur est de toute façon stable sous
|
|
118
|
+
// suppression concurrente. En dry-run le pool ne rétrécit pas : sans curseur, on relisait le
|
|
119
|
+
// même premier lot à chaque tour (P2 neuvième audit : 120 lignes comptées 300). Le lot est rogné
|
|
120
|
+
// au RESTE du plafond (`min(taille, plafond - examinees)`) pour ne jamais le dépasser. Et on
|
|
121
|
+
// compte les lignes RENDUES par le DELETE (`return=representation&select=id`), pas les ids
|
|
122
|
+
// présélectionnés : deux exécutions concurrentes n'annoncent pas deux fois la même suppression.
|
|
71
123
|
async function purgerParLots(table, filtre, colId, { dryRun = false, taille = LOT, plafond = PLAFOND } = {}) {
|
|
72
|
-
let examinees = 0, supprimees = 0;
|
|
73
|
-
const maxTours = Math.max(1, Math.ceil(plafond / taille));
|
|
74
|
-
let tours = 0, tronque = false;
|
|
124
|
+
let examinees = 0, supprimees = 0, tronque = false, curseur = null;
|
|
75
125
|
for (;;) {
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
const
|
|
126
|
+
const reste = plafond - examinees;
|
|
127
|
+
if (reste <= 0) { tronque = await resteEncore(table, filtre, colId, curseur, dryRun); break; }
|
|
128
|
+
const limite = Math.min(taille, reste);
|
|
129
|
+
const borneCur = curseur != null ? `&${colId}=gt.${enc(String(curseur))}` : "";
|
|
130
|
+
const lot = await PLAYER.db.request(`${table}?${filtre}${borneCur}&select=${colId}&order=${colId}.asc&limit=${limite}`);
|
|
79
131
|
if (!Array.isArray(lot) || !lot.length) break;
|
|
80
132
|
examinees += lot.length;
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
await PLAYER.db.request(`${table}?${colId}=in.(${ids.join(",")})`, { method: "DELETE", headers: { Prefer: "return=minimal" } });
|
|
85
|
-
supprimees += ids.length;
|
|
86
|
-
}
|
|
87
|
-
}
|
|
88
|
-
if (lot.length < taille) break; // dernier lot
|
|
89
|
-
if (dryRun) { tronque = tours >= maxTours; if (tours >= maxTours) break; }
|
|
133
|
+
curseur = lot[lot.length - 1][colId];
|
|
134
|
+
supprimees += await effacerParIds(table, colId, lot.map((r) => r[colId]).filter((v) => v != null), { dryRun });
|
|
135
|
+
if (lot.length < limite) break; // dernier lot (moins que demandé → plus rien après)
|
|
90
136
|
}
|
|
91
137
|
return { examinees, supprimees, tronque };
|
|
92
138
|
}
|
|
93
139
|
|
|
140
|
+
// « Reste-t-il une ligne au-delà du curseur ? » — sonde d'UNE ligne. Départage « on s'est arrêté
|
|
141
|
+
// pile au plafond mais tout est parti » de « il reste à faire ». En run réel les lignes lues ont
|
|
142
|
+
// été supprimées : la fenêtre a avancé, on relit depuis le début du filtre (curseur non requis).
|
|
143
|
+
async function resteEncore(table, filtre, colId, curseur, dryRun) {
|
|
144
|
+
const borneCur = dryRun && curseur != null ? `&${colId}=gt.${enc(String(curseur))}` : "";
|
|
145
|
+
const sonde = await PLAYER.db.request(`${table}?${filtre}${borneCur}&select=${colId}&order=${colId}.asc&limit=1`);
|
|
146
|
+
return Array.isArray(sonde) && sonde.length > 0;
|
|
147
|
+
}
|
|
148
|
+
|
|
94
149
|
// ⚠️ REVALIDATION À LA SUPPRESSION — le MÊME validateur que l'écriture (server/presentations.js),
|
|
95
150
|
// avec le slug de la présentation purgée. Une validation d'écriture n'est jamais la seule barrière
|
|
96
151
|
// d'un delete : les lignes déjà en base d'avant le correctif peuvent porter une URL piégée.
|
|
97
152
|
const { cheminPieceJointe: cheminSurSlug } = require("./presentations");
|
|
98
153
|
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
154
|
+
// Purge des messages d'une présentation morte, par lots bornés qui lisent id+attachment ENSEMBLE :
|
|
155
|
+
// on retire les fichiers du bucket du lot (si l'hôte sait), puis on supprime le lot. Rend `tronque`
|
|
156
|
+
// pour que l'appelant décide de garder ou non la présentation. Compte les lignes RENDUES.
|
|
157
|
+
async function purgerMessagesPresentation(slug, opts, base) {
|
|
158
|
+
const { dryRun, taille, plafond } = opts;
|
|
159
|
+
let supprimees = 0, fichiers = 0, fichiersErreur = 0, examinees = 0, tronque = false, curseur = null;
|
|
160
|
+
for (;;) {
|
|
161
|
+
const reste = plafond - examinees;
|
|
162
|
+
if (reste <= 0) { tronque = await resteEncore("doc_presentation_messages", `slug=eq.${enc(slug)}`, "id", curseur, dryRun); break; }
|
|
163
|
+
const limite = Math.min(taille, reste);
|
|
164
|
+
const borneCur = curseur != null ? `&id=gt.${enc(String(curseur))}` : "";
|
|
165
|
+
const lot = await PLAYER.db.request(`doc_presentation_messages?slug=eq.${enc(slug)}${borneCur}&select=id,attachment&order=id.asc&limit=${limite}`);
|
|
166
|
+
if (!Array.isArray(lot) || !lot.length) break;
|
|
167
|
+
examinees += lot.length;
|
|
168
|
+
curseur = lot[lot.length - 1].id;
|
|
169
|
+
for (const j of lot) {
|
|
170
|
+
const url = j.attachment && (typeof j.attachment === "object" ? j.attachment.url : j.attachment);
|
|
171
|
+
const chemin = cheminSurSlug(url, slug, base); // hors du dossier du slug → null → jamais retiré (barrière 2)
|
|
172
|
+
const issue = await retirerFichier(chemin, { dryRun });
|
|
173
|
+
if (issue === true) fichiers += 1; else if (issue === false) fichiersErreur += 1; // false = échec compté
|
|
174
|
+
}
|
|
175
|
+
supprimees += await effacerParIds("doc_presentation_messages", "id", lot.map((r) => r.id).filter((v) => v != null), { dryRun });
|
|
176
|
+
if (lot.length < limite) break;
|
|
177
|
+
}
|
|
178
|
+
return { supprimees, fichiers, fichiersErreur, examinees, tronque };
|
|
179
|
+
}
|
|
180
|
+
|
|
181
|
+
async function purgerRetention(now, optsBrutes = {}) {
|
|
182
|
+
let f, opts;
|
|
183
|
+
try { f = fenetresValidees(); opts = optionsValidees(optsBrutes); }
|
|
102
184
|
catch (e) {
|
|
103
185
|
if (!e.retentionInvalide) throw e;
|
|
104
186
|
try { PLAYER.errors.capture(e, { route: "retention" }); } catch { /* jamais bloquant */ }
|
|
105
|
-
return { ok: false, error: e.message }; // config douteuse → zéro DELETE
|
|
187
|
+
return { ok: false, error: e.message }; // config OU option douteuse → zéro DELETE
|
|
106
188
|
}
|
|
107
189
|
const base = String((PLAYER.config && PLAYER.config.supabaseUrl) || "");
|
|
108
190
|
const rapport = {};
|
|
@@ -125,24 +207,22 @@ async function purgerRetention(now, opts = {}) {
|
|
|
125
207
|
const bPres = borne(now, f.presentationsMois);
|
|
126
208
|
const mortes = await PLAYER.db.request(`doc_presentations?active=eq.false&updated_at=lt.${enc(bPres)}&select=slug&order=slug.asc&limit=${PLAFOND_PRESENTATIONS}`);
|
|
127
209
|
const presRapport = { examinees: 0, supprimees: 0, messages: 0, presences: 0, fichiers: 0, fichiersErreur: 0, tronque: Array.isArray(mortes) && mortes.length >= PLAFOND_PRESENTATIONS };
|
|
128
|
-
const retirer = PLAYER.storage && typeof PLAYER.storage.remove === "function" ? PLAYER.storage.remove.bind(PLAYER.storage) : null;
|
|
129
210
|
for (const p of (Array.isArray(mortes) ? mortes : [])) {
|
|
130
211
|
const slug = p && p.slug; if (!slug) continue;
|
|
131
212
|
presRapport.examinees += 1;
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
if (!opts.dryRun) {
|
|
213
|
+
// Messages : lot borné qui lit id ET attachment ENSEMBLE (la lecture des pièces jointes n'est
|
|
214
|
+
// plus une requête globale non bornée — P2), retire les fichiers du lot, puis supprime le lot.
|
|
215
|
+
const msgs = await purgerMessagesPresentation(slug, opts, base);
|
|
216
|
+
presRapport.messages += msgs.supprimees;
|
|
217
|
+
presRapport.fichiers += msgs.fichiers;
|
|
218
|
+
presRapport.fichiersErreur += msgs.fichiersErreur;
|
|
219
|
+
const pres = opts.dryRun ? { supprimees: 0, tronque: false } : await purgerParLots("doc_presentation_attendees", `slug=eq.${enc(slug)}`, "attendee_key", opts);
|
|
220
|
+
presRapport.presences += pres.supprimees;
|
|
221
|
+
// ⚠️ LA PRÉSENTATION N'EST SUPPRIMÉE QUE SI TOUS SES ENFANTS SONT PARTIS (P2 neuvième audit) :
|
|
222
|
+
// sinon 5 000 messages partent, le parent aussi, et 1 000 orphelins restent que le balayage
|
|
223
|
+
// suivant ne peut plus rattacher à leur présentation. Tronqués → on garde le parent inactif
|
|
224
|
+
// pour le passage suivant.
|
|
225
|
+
if (!opts.dryRun && !msgs.tronque && !pres.tronque) {
|
|
146
226
|
presRapport.supprimees += (await purgerParLots("doc_presentations", `slug=eq.${enc(slug)}`, "slug", opts)).supprimees;
|
|
147
227
|
}
|
|
148
228
|
}
|
package/server/routes-liens.js
CHANGED
|
@@ -32,7 +32,14 @@ async function traiter(req, res, body, slug) {
|
|
|
32
32
|
let admin = false;
|
|
33
33
|
if (!hote) { const u = await PLAYER.identity.verifyToken(req.headers.authorization); admin = !!(u && PLAYER.identity.isAdmin(u)); }
|
|
34
34
|
if (!hote && !admin) return jd(403, { ok: false, error: "retention.run : hôte de confiance ou admin requis" });
|
|
35
|
-
|
|
35
|
+
// ⚠️ LES OPTIONS TRAVERSENT LA ROUTE (P1 neuvième audit) — dryRun compris, sinon la
|
|
36
|
+
// purge « à blanc » supprimait pour de vrai. La validation vit dans purgerRetention
|
|
37
|
+
// (avant tout DELETE) ; un refus (ok:false + error) devient un 400, pas un 200.
|
|
38
|
+
// Tout le corps SAUF `action` est passé au validateur : une clé inconnue (faute de
|
|
39
|
+
// frappe d'option) est ainsi refusée, pas ignorée en silence.
|
|
40
|
+
const { action: _a, ...opts } = body;
|
|
41
|
+
const resultat = await require("./retention").purgerRetention(Date.now(), opts);
|
|
42
|
+
return jd(resultat.ok === false ? 400 : 200, resultat);
|
|
36
43
|
} catch (e) { try { PLAYER.errors.capture(e, { route: "retention" }); } catch { /* jamais bloquant */ } return jd(500, { ok: false }); }
|
|
37
44
|
}
|
|
38
45
|
if (String(body.action || "").startsWith("docshare.")) {
|
|
@@ -457,9 +464,11 @@ async function traiter(req, res, body, slug) {
|
|
|
457
464
|
}
|
|
458
465
|
res.statusCode = 200; res.setHeader("Content-Type", "application/json"); res.end('{"ok":true}');
|
|
459
466
|
return;
|
|
460
|
-
// ⚠️ PAS de `return false` ici : le bloc ci-dessus est le
|
|
461
|
-
//
|
|
462
|
-
//
|
|
467
|
+
// ⚠️ PAS de `return false` ici : le bloc ci-dessus est le repli ANALYTIQUE (open/page/heartbeat/
|
|
468
|
+
// session). Une action inconnue N'Y ARRIVE PLUS — elle est refusée en 400 unknown-action au
|
|
469
|
+
// début de ce repli (neuvième audit : le commentaire disait encore « finit ok:true », c'était
|
|
470
|
+
// faux depuis le huitième). Cette famille répond donc toujours pour un événement valide, et doit
|
|
471
|
+
// rester la DERNIÈRE du dispatch (handler le dit aussi).
|
|
463
472
|
}
|
|
464
473
|
|
|
465
474
|
module.exports = { init, traiter };
|
|
@@ -9,21 +9,30 @@
|
|
|
9
9
|
-- Usage : psql -f supabase/recensement-retention.sql — chaque ligne rend « table|restantes ».
|
|
10
10
|
-- Après une purge, tout doit être à zéro ; un reste est un mensonge de la purge OU un trou de
|
|
11
11
|
-- son périmètre. La forge l'exécute à chaque course, après le banc base.
|
|
12
|
+
--
|
|
13
|
+
-- ⚠️ FENÊTRES PARAMÉTRABLES (neuvième audit) : si un hôte configure d'autres durées via
|
|
14
|
+
-- `config.retention`, le recensement doit contrôler LA MÊME politique, pas les défauts figés.
|
|
15
|
+
-- Les variables psql ci-dessous portent les défauts de RETENTION.md ; passez-les à l'appel pour
|
|
16
|
+
-- une configuration non standard, ex. :
|
|
17
|
+
-- psql -v journaux_mois=6 -v presentations_mois=3 -v liens_mois=6 -f supabase/recensement-retention.sql
|
|
18
|
+
\if :{?journaux_mois}\else \set journaux_mois 13 \endif
|
|
19
|
+
\if :{?presentations_mois}\else \set presentations_mois 12 \endif
|
|
20
|
+
\if :{?liens_mois}\else \set liens_mois 13 \endif
|
|
12
21
|
select 'commercial_doc_views' as perimetre, count(*) as restantes
|
|
13
22
|
from public.commercial_doc_views
|
|
14
|
-
where at < now() -
|
|
23
|
+
where at < now() - make_interval(months => :journaux_mois)
|
|
15
24
|
union all
|
|
16
25
|
select 'commercial_doc_sessions', count(*)
|
|
17
26
|
from public.commercial_doc_sessions
|
|
18
|
-
where last_at < now() -
|
|
27
|
+
where last_at < now() - make_interval(months => :journaux_mois)
|
|
19
28
|
union all
|
|
20
29
|
select 'commercial_doc_internal_sessions', count(*)
|
|
21
30
|
from public.commercial_doc_internal_sessions
|
|
22
|
-
where last_at < now() -
|
|
31
|
+
where last_at < now() - make_interval(months => :journaux_mois)
|
|
23
32
|
union all
|
|
24
33
|
select 'doc_bot_sessions', count(*)
|
|
25
34
|
from public.doc_bot_sessions
|
|
26
|
-
where last_at < now() -
|
|
35
|
+
where last_at < now() - make_interval(months => :journaux_mois)
|
|
27
36
|
union all
|
|
28
37
|
select 'player_rate_limits', count(*)
|
|
29
38
|
from public.player_rate_limits
|
|
@@ -31,11 +40,11 @@ select 'player_rate_limits', count(*)
|
|
|
31
40
|
union all
|
|
32
41
|
select 'commercial_doc_shares_revoques', count(*)
|
|
33
42
|
from public.commercial_doc_shares
|
|
34
|
-
where revoked and revoked_at < now() -
|
|
43
|
+
where revoked and revoked_at < now() - make_interval(months => :liens_mois)
|
|
35
44
|
union all
|
|
36
45
|
select 'doc_presentations_mortes', count(*)
|
|
37
46
|
from public.doc_presentations
|
|
38
|
-
where not active and updated_at < now() -
|
|
47
|
+
where not active and updated_at < now() - make_interval(months => :presentations_mois)
|
|
39
48
|
union all
|
|
40
49
|
-- Messages et présences : orphelins d'une présentation morte-ancienne OU d'aucune présentation —
|
|
41
50
|
-- formulé par NOT EXISTS, pas par la liste de slugs que la purge a construite.
|
|
@@ -44,11 +53,11 @@ select 'doc_presentation_messages_orphelins', count(*)
|
|
|
44
53
|
where not exists (
|
|
45
54
|
select 1 from public.doc_presentations p
|
|
46
55
|
where p.slug = m.slug
|
|
47
|
-
and (p.active or p.updated_at >= now() -
|
|
56
|
+
and (p.active or p.updated_at >= now() - make_interval(months => :presentations_mois)))
|
|
48
57
|
union all
|
|
49
58
|
select 'doc_presentation_attendees_orphelins', count(*)
|
|
50
59
|
from public.doc_presentation_attendees a
|
|
51
60
|
where not exists (
|
|
52
61
|
select 1 from public.doc_presentations p
|
|
53
62
|
where p.slug = a.slug
|
|
54
|
-
and (p.active or p.updated_at >= now() -
|
|
63
|
+
and (p.active or p.updated_at >= now() - make_interval(months => :presentations_mois)));
|