discovery-media-player 0.1.148 → 0.1.149

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.
@@ -486,7 +486,8 @@ above: an unheard-of action answered *no* narrows this view rather than breaking
486
486
  `retentionSweep` says the instance *can* purge; it says nothing about what has piled up. The card
487
487
  gains a `purge` block counting the rows that still carry a reader IP or a raw User-Agent:
488
488
 
489
- "purge": { "borne": 1000, "sessionsIp": 0, "sessionsUa": 0, "vuesUa": 0, "vide": true }
489
+ "purge": { "borne": 5000, "tronque": false, "lignes": { "sessions": 1908, "vues": 3200 },
490
+ "sessionsIp": 0, "sessionsUa": 0, "vuesUa": 0, "vide": true }
490
491
 
491
492
  `vide` is the reading that matters: `true` means nothing of that legacy is left **on this
492
493
  instance's live rows** — the condition under which those columns can eventually be dropped —
@@ -494,9 +495,37 @@ instance's live rows** — the condition under which those columns can eventuall
494
495
  `null` for the same reason: a failed probe must never read as a zero, because zero is the answer
495
496
  that authorises a deletion.
496
497
 
497
- The counts are **bounded** at `borne` rows and read one small column, so `1000` reads as *at least
498
- a thousand*, never as exactly. They run only under `&schema=1`, the mode where you have asked for
499
- the database.
498
+ `lignes` is what the counter **looked at**, per table. A bare `0` cannot tell "purged" from "never
499
+ written" from "the probe is aimed wrong"; the denominator separates them *0 of 1908* means there
500
+ was something to look at, *0 of 0* means the table is empty or out of reach and the zero proves
501
+ nothing. It is `null` on the same terms as the counts.
502
+
503
+ The counts are **bounded** at `borne` rows and read one small column. ⚠️ **`tronque` says whether
504
+ that bound was reached**: when it is `true`, every number in the block is a *lower bound*, not a
505
+ count. Without it a saturated `5000` would be indistinguishable from an exact five thousand — a
506
+ wrong number that reads as right, which is worse than an absent one, because an absence makes you
507
+ look and a number makes you conclude. `vide` stays correct either way: saturation can only make it
508
+ `false`, never wrongly `true`.
509
+
510
+ They run only under `&schema=1`, the mode where you have asked for the database.
511
+
512
+ ⚠️ **The purge attestation is a commitment, not a convenience.** Every column this player empties
513
+ carries a `comment on column` whose text **begins with the exact marker**:
514
+
515
+ VIDE ET PLUS JAMAIS ECRITE depuis la <migration number>.
516
+
517
+ Read it through `col_description()`. It is what *proves* a purge was applied — a count of zero does
518
+ not, since it cannot tell "purged" from "never written". **We commit to two things**: to post it on
519
+ every column a future migration empties, and not to reword that prefix. It is deliberately plain
520
+ ASCII, without accent or apostrophe, so it survives encodings and needs no escaping.
521
+
522
+ This used to be a convenience, designed for a person proving a purge. A host told us its inventory
523
+ now reads it **mechanically**, crossing it with the residual counts to raise an alarm when values
524
+ reappear beside an attestation. That is the moment an artefact becomes an interface — and the reason
525
+ to commit is the failure mode: if we quietly stopped posting it, that alarm would go **silent
526
+ without saying so**, a failure caused here and invisible there. A guard in this repository refuses
527
+ any migration that empties a column without the marker, so undoing the commitment turns something
528
+ red rather than turning something quiet.
500
529
 
501
530
  ⚠️ **Why this exists at all:** our tables live in *your* database, and your audit enumerates *your*
502
531
  tables — a dependency's schema occupies a zone nobody's inventory visits. Two integrating hosts
package/docs/RETENTION.md CHANGED
@@ -242,23 +242,47 @@ is a number.
242
242
  **1. The rows.** Reading logs are deleted **13 months** after `at` / `last_at` by default. A host
243
243
  changes that through `config.retention` — whole months in `[1, 120]`.
244
244
 
245
- ⚠️ **But the automatic sweep is strictly opt-in.** It runs only where a host has written
246
- `config.retention.balayage: true`; the `retention.run` action stays available without opt-in, because
247
- calling it *is* the decision. **On a host that has enabled neither, no row has ever been deleted, and
248
- the 13 months describe an intent rather than an event.** Anyone attesting a retention period should
249
- check which of the two is true of the installation in front of them, rather than quoting the default.
245
+ ⚠️ **But the automatic sweep is strictly opt-in**, and there are **three** states, not two — an
246
+ integrating host measured the one we had left out:
247
+
248
+ | state | how to tell | what you may claim |
249
+ |---|---|---|
250
+ | **off** | `retentionSweep` false, and no `retention.run` in your logs | nothing has ever been deleted; the window is a policy you have not applied |
251
+ | **armed, never exercised** | armed, but no row has yet reached the window — check the age of your oldest row against it | nothing has ever been deleted **either**, and not for want of configuration |
252
+ | **armed, and has deleted** | armed, and a run reported non-zero counts | the window is an *event*, and only here |
253
+
254
+ ⚠️ **The middle state is the misleading one**, because it has every appearance of the good one: armed,
255
+ correct, and indistinguishable in its effects from being off — no deletion, no log, no evidence it
256
+ works. A host reported exactly this: sweep armed, oldest row 63 days old, **zero rows past 13 months
257
+ out of 1908**. Its first real execution will be roughly **eighteen months after it was armed**, on
258
+ data nobody will have looked at, never having run in anger. Treat it as what it is — a guard that has
259
+ never been exercised, with a deadline — and exercise it deliberately before then, on a copy or with
260
+ `retention.run` and a short window, rather than discovering its behaviour the day it matters.
261
+
262
+ Anyone attesting a retention period should establish which of the **three** is true of the
263
+ installation in front of them, rather than quoting the default.
250
264
 
251
265
  **2. The values inside surviving rows.** Erased by 0026 and 0027 as soon as they are applied, and
252
266
  physically gone from the table once routine autovacuum has passed — no operator action, typically
253
267
  minutes to hours on an active table. This part does not wait for the 13 months.
254
268
 
255
269
  **3. Backups, write-ahead logs, exports and migration dumps.** **Outside this player's reach, and we
256
- neither set nor observe them.** They follow the hosting platform's own settings — on a managed
257
- provider, typically a point-in-time-recovery window plus a snapshot schedule, each with its own
258
- retention. A purge is complete end to end at *the later of*: the day 0026/0027 were applied plus the
259
- host's longest backup retention, and for the rows themselves — whichever purge the host actually
260
- runs. **Ask the platform for two numbers: the PITR window and the oldest retained snapshot.** Until
261
- both have rolled past the migration date, earlier copies still hold the erased values.
270
+ neither set nor observe them.** They follow the hosting platform's own settings — typically a
271
+ point-in-time-recovery window plus a snapshot schedule, each with its own retention. Ask the platform
272
+ for **the PITR window and the age of the oldest retained snapshot**; until they have rolled past the
273
+ day 0026/0027 were applied, earlier copies still hold the erased values.
274
+
275
+ ⚠️ **Neither number is exposed by any API, and that matters more than it looks.** Two integrating
276
+ hosts checked independently, on two different toolsets: the provider's API and its MCP tools return
277
+ region, status and engine version — nothing about backups. **A human has to read them from the
278
+ dashboard.** This is written here because the instruction above is *executable in appearance*: an
279
+ agent following it will look for a tool, find none, and then either stop — or, the real risk, report
280
+ the purge complete having skipped the one step it could not measure. If you cannot produce these two
281
+ numbers, say so; do not round the sentence.
282
+
283
+ ⚠️ **And it is not always "the later of two".** An option that is not subscribed retains nothing, so
284
+ it defers nothing. A host with no PITR and eight daily snapshots has **one** deadline, not two: the
285
+ age of its oldest snapshot. Take the later of the deadlines that *exist*.
262
286
 
263
287
  ## Limits stated rather than left unsaid
264
288
 
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "discovery-media-player",
3
- "version": "0.1.148",
3
+ "version": "0.1.149",
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",
@@ -411,8 +411,18 @@ function tick() {
411
411
  * ⚠️ ON COMPTE DES LIGNES, PAS UN `count=exact`. La capacité `db` de l'hôte rend le corps de la
412
412
  * réponse, pas ses en-têtes : le compte de PostgREST voyage dans `Content-Range`, donc il serait
413
413
  * illisible sans élargir le contrat d'hôte — ce qu'un compteur de diagnostic ne justifie pas.
414
- * D'où un comptage BORNÉ : au plus `BORNE_RESTE` identifiants, une seule petite colonne. Atteindre
415
- * la borne se lit « au moins autant », jamais « exactement ».
414
+ * D'où un comptage BORNÉ : au plus `BORNE_RESTE` identifiants, une seule petite colonne.
415
+ *
416
+ * ⚠️ ET LA SATURATION SE DIT, ELLE NE SE DEVINE PAS — deux hôtes ont trouvé ce défaut dans la
417
+ * première version, le même jour, indépendamment. Elle demandait `limit=BORNE` et publiait
418
+ * `lignes.length` : sur une base portant cinq mille adresses, elle rendait `1000`, que rien ne
419
+ * distinguait d'un compte exact de mille. Un nombre faux qui se lit comme juste — pire qu'un
420
+ * nombre absent, parce que l'absence fait chercher et que le nombre fait conclure.
421
+ *
422
+ * Le remède vivait à trois cents lignes d'ici : `purgerRetention` rend `tronque` depuis toujours,
423
+ * pour exactement cette raison. On demande donc `BORNE + 1` : en recevoir autant prouve qu'il en
424
+ * reste, sans coûter une ligne de plus. `n` reste plafonné à la borne, et `tronque` dit qu'il faut
425
+ * le lire « au moins ».
416
426
  *
417
427
  * ⚠️ ET LE COÛT EST INVERSE DE L'INTUITION, donc il est dit plutôt que caché : quand il reste
418
428
  * beaucoup de lignes, la base s'arrête à la borne et c'est rapide ; quand il n'en reste AUCUNE,
@@ -423,7 +433,13 @@ function tick() {
423
433
  * ⚠️ UN ÉCHEC REND `null`, JAMAIS ZÉRO. Zéro est la réponse qui autorise à supprimer une colonne :
424
434
  * la fabriquer à partir d'une sonde en panne serait le pire mensonge que cette carte puisse faire.
425
435
  */
426
- const BORNE_RESTE = 1000;
436
+ // ⚠️ CINQ MILLE, ET LE NOMBRE VIENT D'UNE MESURE. Il valait mille, et le banc écrit avec les
437
+ // volumes RÉELS d'un hôte l'a fait rougir : sa table de vues en portait 1651. La borne saturait
438
+ // donc dès le premier jour chez lui, et un compteur qui plafonne sous les volumes qu'il est censé
439
+ // décrire ne décrit rien. Cinq mille couvre les deux hôtes connus avec de la marge, reste une
440
+ // seule petite colonne à transférer, et `tronque` dit le reste. La borne est un plafond de COÛT,
441
+ // pas une opinion sur ce qu'un hôte peut avoir.
442
+ const BORNE_RESTE = 5000;
427
443
 
428
444
  const SONDES_RESTE = [
429
445
  ["sessionsIp", "commercial_doc_sessions", "session_id", "ip"],
@@ -431,6 +447,10 @@ const SONDES_RESTE = [
431
447
  ["vuesUa", "commercial_doc_views", "id", "ua"],
432
448
  ];
433
449
 
450
+ /** Les tables regardées, pour le dénominateur — une par table, pas une par sonde. */
451
+ const TABLES_RESTE = [["sessions", "commercial_doc_sessions", "session_id"],
452
+ ["vues", "commercial_doc_views", "id"]];
453
+
434
454
  /**
435
455
  * ⚠️ ET LA COLONNE DISPARUE EST UN ÉTAT CONNU, PAS UNE PANNE. Le jour où un exploitant supprime ces
436
456
  * colonnes — le geste que ce compteur sert à autoriser — la requête échoue avec le
@@ -443,27 +463,67 @@ const SONDES_RESTE = [
443
463
  */
444
464
  const COLONNE_ABSENTE = "42703";
445
465
 
446
- async function compterReste(table, cle, colonne) {
466
+ /** `{ n, tronque }` — `n` nul veut dire indéterminé, jamais zéro. */
467
+ const compte = (n, tronque) => ({ n, tronque });
468
+
469
+ async function compterBorne(chemin) {
447
470
  try {
448
- const lignes = await PLAYER.db.request(
449
- `${table}?select=${cle}&${colonne}=not.is.null&limit=${BORNE_RESTE}`,
450
- { timeoutMs: 8000 },
451
- );
452
- return Array.isArray(lignes) ? lignes.length : null;
471
+ // ⚠️ BORNE + 1 : la ligne excédentaire ne sert qu'à PROUVER qu'il en reste. On ne la publie pas.
472
+ const lignes = await PLAYER.db.request(`${chemin}&limit=${BORNE_RESTE + 1}`, { timeoutMs: 8000 });
473
+ if (!Array.isArray(lignes)) return compte(null, false);
474
+ return compte(Math.min(lignes.length, BORNE_RESTE), lignes.length > BORNE_RESTE);
453
475
  } catch (e) {
454
- if (e && e.details && e.details.code === COLONNE_ABSENTE) return 0;
455
- return null; // indéterminé — surtout pas zéro
476
+ if (e && e.details && e.details.code === COLONNE_ABSENTE) return compte(0, false);
477
+ return compte(null, false); // indéterminé — surtout pas zéro
456
478
  }
457
479
  }
458
480
 
481
+ const compterReste = (table, cle, colonne) =>
482
+ compterBorne(`${table}?select=${cle}&${colonne}=not.is.null`);
483
+
484
+ /**
485
+ * ⚠️ ET LE COMPTEUR PORTE CE QU'IL A REGARDÉ — un hôte nous l'a demandé, et il avait raison.
486
+ *
487
+ * `sessionsIp: 0` ne distingue pas trois choses : « purgé », « jamais écrit », et « la sonde vise à
488
+ * côté ». Les deux premières se valent pour qui veut supprimer une colonne ; la troisième est un
489
+ * mensonge. Le dénominateur les sépare : « 0 sur 1908 lignes examinées » dit qu'il y avait quelque
490
+ * chose à regarder, « 0 sur 0 » dit que la table est vide ou hors d'atteinte et que le zéro ne
491
+ * prouve rien.
492
+ *
493
+ * C'est notre propre règle anti-vacuité — un plancher compte la FORME RECONNUE, pas les choses
494
+ * comptées — appliquée partout dans `tools/` et absente d'ici jusqu'à ce qu'un lecteur la réclame.
495
+ *
496
+ * ⚠️ ET IL NE COÛTE PRESQUE RIEN, à l'inverse du compte filtré : sans filtre, la base s'arrête à la
497
+ * borne dès les premières lignes. Une par TABLE, pas une par sonde — deux des trois colonnes vivent
498
+ * dans la même.
499
+ */
500
+ const compterLignes = (table, cle) => compterBorne(`${table}?select=${cle}`);
501
+
459
502
  async function resteDeLaPurge() {
460
- const comptes = await Promise.all(SONDES_RESTE.map(([, t, c, col]) => compterReste(t, c, col)));
461
- const out = { borne: BORNE_RESTE };
462
- SONDES_RESTE.forEach(([nom], i) => { out[nom] = comptes[i]; });
503
+ const [comptes, totaux] = await Promise.all([
504
+ Promise.all(SONDES_RESTE.map(([, t, c, col]) => compterReste(t, c, col))),
505
+ Promise.all(TABLES_RESTE.map(([, t, c]) => compterLignes(t, c))),
506
+ ]);
507
+ // ⚠️ ACCUMULATEURS NUS, comme celui de `fenetresValidees` plus haut et pour la même raison : la
508
+ // garde de forme reconnaît `Object.create(null)`, et une écriture indexée par autre chose qu'un
509
+ // littéral n'a alors aucun prototype à polluer. Les clés viennent ici de constantes du fichier,
510
+ // mais un objet nu ne coûte rien et la propriété se lit sans avoir à remonter leur provenance.
511
+ const parTable = Object.create(null);
512
+ TABLES_RESTE.forEach(([nom], i) => { parTable[nom] = totaux[i].n; });
513
+ const out = Object.create(null);
514
+ out.borne = BORNE_RESTE;
515
+ // ⚠️ UN SEUL DRAPEAU POUR TOUT LE BLOC, parce qu'il ne sert qu'à une chose : dire au lecteur que
516
+ // les nombres qu'il voit sont des minorants. Un drapeau par compte suggérerait qu'on peut faire
517
+ // confiance aux autres, alors que la borne est commune et que la question ne l'est pas.
518
+ out.tronque = [...comptes, ...totaux].some((c) => c.tronque);
519
+ out.lignes = parTable;
520
+ SONDES_RESTE.forEach(([nom], i) => { out[nom] = comptes[i].n; });
463
521
  // ⚠️ TROIS ÉTATS, PAS DEUX. `true` : plus rien, le retrait des colonnes est permis ICI. `false` :
464
522
  // il reste des lignes. `null` : au moins une sonde n'a pas répondu — on ne sait pas, et « on ne
465
523
  // sait pas » ne doit jamais se lire comme « c'est bon ».
466
- out.vide = comptes.some((n) => n === null) ? null : comptes.every((n) => n === 0);
524
+ // ⚠️ `vide` RESTE JUSTE MÊME SATURÉ, et c'est ce qui compte : c'est le champ qui autorise le
525
+ // retrait d'une colonne, et la saturation ne peut le rendre que FAUX — jamais vrai à tort.
526
+ out.vide = comptes.some((c) => c.n === null) ? null : comptes.every((c) => c.n === 0);
467
527
  return out;
468
528
  }
469
529