discovery-media-player 0.1.151 → 0.1.153

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.
@@ -50,7 +50,7 @@ need.
50
50
  "presenceDurcissement": "inconnu",
51
51
  "presenceFusion": "inconnu",
52
52
  "lectureSaturee": { "total": 0, "fenetreS": 0, "derniereIlYaS": null },
53
- "mesures": { "fenetreS": 0, "seauxMs": [1, 2, 5, 10, 25, 50, 100, 250, 500, 1000, 2500, 5000, 10000], "routes": {}, "base": { "n": 0 }, "statuts": { "ok": 0, "refus4xx": 0, "debit429": 0, "occupe503": 0, "erreur5xx": 0 }, "memoireMio": { "rss": 0, "heap": 0, "tampons": 0 }, "boucleMs": { "n": 0, "moyen": null, "p99": null, "resolutionMs": 20 } },
53
+ "mesures": { "fenetreS": 0, "seauxMs": [1, 2, 5, 10, 25, 50, 100, 250, 500, 1000, 2500, 5000, 10000], "familles": ["document", "presentation", "action", "fichier", "carte", "autre"], "routes": {}, "base": { "n": 0 }, "statuts": { "ok": 0, "refus4xx": 0, "debit429": 0, "occupe503": 0, "erreur5xx": 0 }, "memoireMio": { "rss": 0, "heap": 0, "tampons": 0 }, "boucleMs": { "n": 0, "moyen": null, "p99": null, "resolutionMs": 20 } },
54
54
  "retentionSweep": false,
55
55
  "hostShare": true,
56
56
  "hostMail": true,
@@ -180,6 +180,7 @@ these numbers from their side.
180
180
  |---|---|
181
181
  | `fenetreS` | seconds this process has been running — **the window every total below was counted over** |
182
182
  | `seauxMs` | the bucket ladder the percentiles are read off, published **with** the numbers |
183
+ | `familles` | **the denominator of `routes`** — every family this build measures, whether or not it was exercised. It does not move with traffic; that is what makes it a denominator |
183
184
  | `routes` | one entry per family of work — `document`, `presentation`, `action`, `fichier`, `carte`, `autre`. Families absent from the object were never exercised in this process |
184
185
  | `base` | the same shape, for calls through the `db` capability **you** supply — measured at the seam, so it covers every call, including ones nobody has written yet |
185
186
  | `statuts` | responses by class: `ok` (<400), `refus4xx`, `debit429`, `occupe503`, `erreur5xx` |
@@ -194,6 +195,17 @@ reading is. `null` means *past the top of the ladder* (over 10 s), which is itse
194
195
  ⚠️ **`n: 0` is not `0 ms`.** A family that was never exercised reports `{ "n": 0 }` and nothing
195
196
  else, and `boucleMs` with no samples reports `moyen: null` — not a zero that would read as *healthy*.
196
197
 
198
+ ⚠️ **`routes: {}` used to be indistinguishable from broken instrumentation, and `familles` is why
199
+ it no longer is.** Omitting an unexercised family is correct — a `0 ms` would read as *instantaneous*
200
+ — but the omission left a reader unable to tell *no traffic in this window* from *measurement is not
201
+ running*. A loaded host never meets the question: its entries are always there, so their presence
202
+ witnesses itself.
203
+
204
+ The rule came from a host measuring their own smallness: **a field whose value is its own witness at
205
+ scale needs an explicit witness at small scale.** At 99 sessions nothing witnesses anything, and a
206
+ busier host is better instrumented without having instrumented anything. `statuts` and `boucleMs`
207
+ already carried their denominators; `routes` had not.
208
+
197
209
  ⚠️ **`boucleMs` is the delay, not the interval.** The sampler observes how long its own timer
198
210
  actually took, which at rest equals its resolution; the resolution is subtracted, so an idle
199
211
  instance reports about `0` rather than a permanent `20` that would send you hunting a fault that
@@ -487,7 +499,7 @@ above: an unheard-of action answered *no* narrows this view rather than breaking
487
499
  gains a `purge` block counting the rows that still carry a reader IP or a raw User-Agent:
488
500
 
489
501
  "purge": { "borne": 5000, "tronque": false, "lignes": { "sessions": 1908, "vues": 3200 },
490
- "sessionsIp": 0, "sessionsUa": 0, "vuesUa": 0, "vide": true }
502
+ "sessionsIp": 0, "sessionsUa": 0, "vuesUa": 0, "vide": true, "voie": "bornee" }
491
503
 
492
504
  `vide` is the reading that matters: `true` means nothing of that legacy is left **on this
493
505
  instance's live rows** — the condition under which those columns can eventually be dropped —
@@ -545,6 +557,24 @@ portability rule refuses.
545
557
  column. Anything that is not a non-negative integer (a string, a float, `undefined`, `NaN`) is read
546
558
  as "no answer" and the player falls back rather than believing it.
547
559
 
560
+ ⚠️ **And `voie` tells you which route produced the numbers, because the numbers cannot.** `"exact"`,
561
+ `"bornee"`, or `"mixte"` when both served the same read — which happens for real: a host mid-purge
562
+ whose column is already dropped makes `count` throw on the filtered paths and answer on the same
563
+ table's totals.
564
+
565
+ This field exists because two hosts, independently and on the same day, found they could not verify
566
+ their own seam. An exact count and an untruncated bounded count render an **identical** block —
567
+ same numbers, same `tronque`, same `vide`. So a `db.count` that returns a string, or a float, falls
568
+ back **silently** and the card looks exactly like a working one: you believe your seam is wired
569
+ when it is not. One host caught it only by luck of volume — their table held 1655 rows, and the
570
+ bounded route is structurally incapable of exceeding the 1000-row ceiling, so the number itself
571
+ happened to be proof. Under a thousand rows there is no such luck, and the other host, whose
572
+ volumes are small, could not tell at all.
573
+
574
+ **Check `voie`, not a value.** A control that distinguishes two *mechanisms* must rest on something
575
+ one can do and the other cannot; a value both could return proves nothing, and a value written down
576
+ from yesterday's measurement rots without anyone touching anything.
577
+
548
578
  ⚠️ **The two alternatives were measured at a host, not assumed here** — recorded so nobody proposes
549
579
  them again in six months believing they were never tried. **`?select=count()` is dead**:
550
580
  `db-aggregates-enabled` is `false` by default, verified on two distinct Supabase projects, and the
@@ -863,6 +893,64 @@ cannot. Setting the number spares you that reasoning entirely.
863
893
  but *did you measure exactly what fails*. Two true statements about the same instance can describe
864
894
  different responses.
865
895
 
896
+ ## What you can see and we cannot
897
+
898
+ **Read this only if you run this player somewhere.** If you are evaluating it, skip to Versioning —
899
+ this section asks for nothing from you.
900
+
901
+ ⚠️ **Everything below exists because three defects in one week were found by hosts, not by us**, and
902
+ none of the three was findable from here. Every guard here measures this repository. They
903
+ cannot measure an installation they have never seen, and that is not a gap we can close by adding
904
+ one more. So this is not a request for feedback in general — it is four specific questions we cannot
905
+ answer ourselves, each printed with what it cost us not to have asked it earlier.
906
+
907
+ You owe us none of this. But you are the only one who can answer any of it.
908
+
909
+ **1. A ceiling in your installation that we assume away.** PostgREST's `db-max-rows` is set to 1000
910
+ by default on Supabase: the server returns 1000 rows however many you ask for. We compared what we
911
+ received against *our* bound, concluded "not truncated", and published `1000` rows of a 1651-row
912
+ table **asserting the number was exact** — which is worse than the defect it replaced, because the
913
+ previous version claimed nothing. Four hours after the release, a host measured it. If your
914
+ deployment caps, times out, paginates, or rewrites anything between us and your database, that
915
+ limit is invisible from here and our arithmetic is probably wrong about it.
916
+
917
+ **2. Something this card asserts that you can check against your own database.** Not "does it look
918
+ right" — *does this number match what a query returns right now*. The purge card is the obvious one:
919
+ `vide: true` is what authorises dropping a column, so a wrong `true` is expensive and a wrong `0`
920
+ authorises a deletion. A host who compared the card against their own tables is the reason the
921
+ counter stopped being able to lie.
922
+
923
+ **3. What your volumes hide from you — and what they reveal for free.** This one is the least
924
+ obvious and cost us a field. At 1655 rows an exact count **is its own proof**: it exceeds a ceiling
925
+ the bounded route structurally cannot cross, so the number itself witnesses which mechanism ran. At
926
+ 99 sessions no value can ever separate the two, and a broken seam is indistinguishable from a
927
+ working one. Same code, same card — one installation detects a failure for free that the other
928
+ cannot see at all. So: **tell us your orders of magnitude**, and tell us when a field of ours only
929
+ makes sense at your scale. A small host is not merely short of occasions; it has lost discriminating
930
+ power, and that is our problem to fix with an explicit witness, not theirs to live with.
931
+
932
+ **4. A rule you drew from your own defect.** The most useful things we received this week were not
933
+ bug reports. They were sentences: *a control that separates two mechanisms must rest on what one can
934
+ do and the other cannot, never on a value both could return* — and *is there a state of the world
935
+ where this value is false?*, which is the question separating a limit of observation from a silent
936
+ failure mode. Both were written by hosts about their own mistakes. Both, applied to this repository
937
+ within the hour, found something. A defect tells us about one line; a rule tells us where to look.
938
+
939
+ ⚠️ **And tell us what you did not do.** A host closed a report with thirty-two reads still
940
+ unaudited, said so plainly, and that sentence is the only reason we know those reads exist. We
941
+ cannot measure your code. We can only know what someone wrote down. *"Not measured"* and *"nothing
942
+ found"* are different sentences, and only one of them is honest when you have not looked.
943
+
944
+ **What not to send.** Shapes and counts, never contents. No row data, no reader IPs or User-Agents —
945
+ those are the columns half this contract exists to get rid of — no keys, tokens, connection strings,
946
+ or private hostnames. *"A table of ~1600 rows returned 1000"* is the whole of what we needed to fix
947
+ the ceiling defect; the rows themselves would have added nothing and created a problem.
948
+
949
+ **What we do with it.** Every item above became code, a bench that dies if the fix is removed, and a
950
+ dated entry naming the case. That last part is deliberate and a host asked for it: an abstract
951
+ justification rots, a dated incident does not. In six months someone will read a field and ask why
952
+ it exists, and the answer will name you.
953
+
866
954
  ## Versioning
867
955
 
868
956
  Semantic versioning on the package, independent of the `contract` number. Pin an **exact** version:
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "discovery-media-player",
3
- "version": "0.1.151",
3
+ "version": "0.1.153",
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/mesures.js CHANGED
@@ -180,6 +180,22 @@ function relever() {
180
180
  // ⚠️ L'ÉCHELLE EST PUBLIÉE AVEC LES CHIFFRES. Sans elle, `p95sousMs: 250` ne dit pas si la
181
181
  // mesure suivante aurait pu être 251 ou 999 — un lecteur ne peut pas juger de sa précision.
182
182
  seauxMs: SEAUX_MS,
183
+ // ⚠️ LE DÉNOMINATEUR DE `routes`, ET IL MANQUAIT DEPUIS LE DÉBUT. Une famille sans échantillon
184
+ // est OMISE de `routes` — donc `routes: {}` ne distingue pas « aucun trafic sur la fenêtre »
185
+ // de « la mesure ne tourne pas ». Chez un hôte chargé la question ne se pose jamais : il y a
186
+ // toujours des entrées, et leur présence témoigne d'elle-même. Chez un hôte à 99 sessions,
187
+ // rien ne témoigne de rien.
188
+ //
189
+ // La règle vient d'un hôte, sur ses propres volumes : « les champs dont la valeur est son
190
+ // propre témoin à grande échelle ont besoin d'un témoin explicite à petite échelle ». Une
191
+ // petite installation n'est pas seulement privée d'occasions — elle perd du POUVOIR
192
+ // DISCRIMINANT, et un hôte plus chargé se trouve mieux instrumenté sans avoir rien instrumenté.
193
+ //
194
+ // ⚠️ ET LES DEUX CHAMPS VOISINS AVAIENT DÉJÀ RAISON, ce qui rend l'omission mesurable plutôt
195
+ // qu'opinable : `statuts` publie ses cinq clés à zéro, `boucleMs` publie `n: 0` avec des
196
+ // `null` explicites « plutôt que de publier un zéro qui se lirait la boucle est saine ». Trois
197
+ // champs frères du même objet, deux qui portent leur dénominateur et un qui l'oubliait.
198
+ familles: [...FAMILLES],
183
199
  routes,
184
200
  base: centiles(histoBase),
185
201
  statuts: { ...statuts },
@@ -477,8 +477,21 @@ const TABLES_RESTE = [["sessions", "commercial_doc_sessions", "session_id"],
477
477
  */
478
478
  const COLONNE_ABSENTE = "42703";
479
479
 
480
- /** `{ n, tronque }` — `n` nul veut dire indéterminé, jamais zéro. */
481
- const compte = (n, tronque) => ({ n, tronque });
480
+ /**
481
+ * `{ n, tronque, voie }` — `n` nul veut dire indéterminé, jamais zéro.
482
+ *
483
+ * ⚠️ ET `voie` NOMME LE MÉCANISME QUI A PRODUIT LE NOMBRE, parce que le nombre seul ne le dit pas.
484
+ * Un compte exact et un compte borné NON tronqué rendent le même JSON : deux hôtes l'ont relevé le
485
+ * même jour, l'un en constatant qu'il ne pouvait pas vérifier sa propre couture, l'autre en
486
+ * écrivant un contrôle qui n'a marché que par chance de volume — sa table dépassait mille, donc la
487
+ * voie par lignes était structurellement incapable de rendre son chiffre. Sous mille, personne ne
488
+ * peut trancher, et un `db.count` qui rend une chaîne retombe SILENCIEUSEMENT sur la voie bornée :
489
+ * l'hôte croit sa couture branchée alors qu'elle ne sert pas.
490
+ */
491
+ const compte = (n, tronque, voie) => ({ n, tronque, voie });
492
+
493
+ const VOIE_EXACTE = "exact";
494
+ const VOIE_BORNEE = "bornee";
482
495
 
483
496
  /**
484
497
  * ⚠️ « MOINS QUE DEMANDÉ » NE PROUVE PAS LA FIN — ET C'EST UN HÔTE RÉEL QUI L'A MONTRÉ.
@@ -577,23 +590,24 @@ async function compterBorne(chemin, cle) {
577
590
  // méthode par lignes, pas celle-ci. `tronque: false` garde donc le sens qu'il a partout —
578
591
  // « lisez ce nombre comme exact » — au lieu d'en prendre un second selon la voie employée.
579
592
  const exact = await compteExact(chemin);
580
- if (exact !== null) return compte(exact, false);
593
+ if (exact !== null) return compte(exact, false, VOIE_EXACTE);
581
594
  try {
582
595
  // ⚠️ BORNE + 1 : la ligne excédentaire ne sert qu'à PROUVER qu'il en reste. On ne la publie pas.
583
596
  // ⚠️ ET L'ORDRE N'EST PAS DÉCORATIF : sans lui, « la dernière ligne reçue » ne désigne aucune
584
597
  // frontière, et le curseur de la sonde ne voudrait rien dire.
585
598
  const lignes = await PLAYER.db.request(
586
599
  `${chemin}&order=${cle}.asc&limit=${BORNE_RESTE + 1}`, { timeoutMs: 8000 });
587
- if (!Array.isArray(lignes)) return compte(null, false);
600
+ if (!Array.isArray(lignes)) return compte(null, false, VOIE_BORNEE);
588
601
  // Notre propre borne atteinte : la preuve est dans la ligne excédentaire, rien à demander.
589
- if (lignes.length > BORNE_RESTE) return compte(BORNE_RESTE, true);
602
+ if (lignes.length > BORNE_RESTE) return compte(BORNE_RESTE, true, VOIE_BORNEE);
590
603
  // Zéro ligne : la sonde au-delà rendrait zéro elle aussi et n'apprendrait rien — y compris sous
591
604
  // un plafond à zéro, que ni l'une ni l'autre ne distingue d'une table vide.
592
- if (!lignes.length) return compte(0, false);
593
- return compte(lignes.length, await resteApres(chemin, cle, lignes[lignes.length - 1][cle]));
605
+ if (!lignes.length) return compte(0, false, VOIE_BORNEE);
606
+ return compte(lignes.length, await resteApres(chemin, cle, lignes[lignes.length - 1][cle]),
607
+ VOIE_BORNEE);
594
608
  } catch (e) {
595
- if (e && e.details && e.details.code === COLONNE_ABSENTE) return compte(0, false);
596
- return compte(null, false); // indéterminé — surtout pas zéro
609
+ if (e && e.details && e.details.code === COLONNE_ABSENTE) return compte(0, false, VOIE_BORNEE);
610
+ return compte(null, false, VOIE_BORNEE); // indéterminé — surtout pas zéro
597
611
  }
598
612
  }
599
613
 
@@ -636,6 +650,18 @@ async function resteDeLaPurge() {
636
650
  // confiance aux autres, alors que la borne est commune et que la question ne l'est pas.
637
651
  out.tronque = [...comptes, ...totaux].some((c) => c.tronque);
638
652
  out.lignes = parTable;
653
+ // ⚠️ UNE SEULE RÉPONSE POUR LES CINQ COMPTES, ET TROIS ÉTATS PLUTÔT QUE DEUX. La question qu'un
654
+ // hôte se pose est « ma couture sert-elle ? », pas « laquelle des cinq ». `"mixte"` n'est pas une
655
+ // commodité : il arrive vraiment — un `count` qui lève sur le chemin d'une colonne supprimée et
656
+ // répond sur le total de la même table — et c'est précisément le cas qu'un drapeau binaire
657
+ // aurait dû arrondir dans un sens ou dans l'autre, donc mentir.
658
+ //
659
+ // ⚠️ CE CHAMP NE DIT RIEN SUR LA JUSTESSE DES NOMBRES, seulement sur leur provenance. Il ne
660
+ // double aucun autre : `tronque` vaut `false` sur les DEUX voies, c'est même toute la raison
661
+ // d'être de cette ligne.
662
+ const voies = [...comptes, ...totaux].map((c) => c.voie);
663
+ out.voie = voies.every((v) => v === VOIE_EXACTE) ? VOIE_EXACTE
664
+ : voies.every((v) => v === VOIE_BORNEE) ? VOIE_BORNEE : "mixte";
639
665
  SONDES_RESTE.forEach(([nom], i) => { out[nom] = comptes[i].n; });
640
666
  // ⚠️ TROIS ÉTATS, PAS DEUX. `true` : plus rien, le retrait des colonnes est permis ICI. `false` :
641
667
  // il reste des lignes. `null` : au moins une sonde n'a pas répondu — on ne sait pas, et « on ne