discovery-media-player 0.1.131 → 0.1.134
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/README.md +1 -0
- package/docs/HOST-CONTRACT.md +51 -1
- package/package.json +1 -1
- package/server/erreurs-base.js +58 -0
- package/server/presentations.js +3 -2
- package/server/retention.js +33 -1
- package/server/routes-agent.js +39 -6
- package/server/routes-liens.js +5 -4
package/README.md
CHANGED
|
@@ -16,6 +16,7 @@ to a third-party SaaS.
|
|
|
16
16
|
[](https://github.com/Juli1artha/discovery-media-player/actions/workflows/ci.yml)
|
|
17
17
|
[](https://github.com/Juli1artha/discovery-media-player/actions/workflows/codeql.yml)
|
|
18
18
|
[](https://scorecard.dev/viewer/?uri=github.com/Juli1artha/discovery-media-player)
|
|
19
|
+
[](https://www.bestpractices.dev/projects/14197)
|
|
19
20
|
[](https://www.npmjs.com/package/discovery-media-player)
|
|
20
21
|
[](https://github.com/Juli1artha/discovery-media-player/pkgs/container/discovery-media-player)
|
|
21
22
|
[](package.json)
|
package/docs/HOST-CONTRACT.md
CHANGED
|
@@ -391,13 +391,63 @@ The rule underneath, safer than the list: **never fall back on a refusal of *acc
|
|
|
391
391
|
back on an inability to *reach*.** And "do not fall back" applies to what you **offer** — an
|
|
392
392
|
"Open ↗" button left in place is falling back one second later.
|
|
393
393
|
|
|
394
|
-
##
|
|
394
|
+
## Four things that will bite
|
|
395
395
|
|
|
396
396
|
**Your document-opening doors reappear.** A host has more than one place that opens a file, and new
|
|
397
397
|
ones get written. Keep the list and hunt it periodically — and note that **your search criteria
|
|
398
398
|
decide what you find**: search by what the user *obtains* (a document opens), not by the technique
|
|
399
399
|
you expect to see.
|
|
400
400
|
|
|
401
|
+
**A row written on behalf of a session also carries the document.** The player hands every bot
|
|
402
|
+
plugin call both the `sessionId` and the `share`. If your plugin stores only the session, then the
|
|
403
|
+
day a session-binding defect is found — one was in 0.1.131, and four more actions in 0.1.133 — you
|
|
404
|
+
cannot say whether anything crossed. The honest answer is not *"nothing found"*, it is **"not
|
|
405
|
+
measurable"**, and those are different sentences to give a client.
|
|
406
|
+
|
|
407
|
+
Measured on a production host the day the second defect was fixed:
|
|
408
|
+
|
|
409
|
+
| table | what it records beside the session | verdict |
|
|
410
|
+
| --- | --- | --- |
|
|
411
|
+
| leads (`bot-contact`, `bot-book`) | the share **and** the document | 46 leads, 21 carrying contact details — **none crossed** |
|
|
412
|
+
| messages (`bot-say`) | the session alone | 1 693 messages — **not measurable** |
|
|
413
|
+
|
|
414
|
+
Same incident, same instance, two verdicts — decided months earlier by one column. It costs nothing
|
|
415
|
+
at write time, and it decides what you are able to say afterwards.
|
|
416
|
+
|
|
417
|
+
⚠️ **The expectation is answerability, not a column name** — and this paragraph earned that sentence
|
|
418
|
+
the hard way, one day after it was written. The same host then swept all eight of their tables
|
|
419
|
+
carrying a session: seven already recorded the document, under four different names (`doc_id`,
|
|
420
|
+
`share_slug`, `link_id`, `xp_id`). **One** did not. The discipline was everywhere and written
|
|
421
|
+
nowhere, which is exactly why it gave way at the single place nobody thought about.
|
|
422
|
+
|
|
423
|
+
Their first sweep looked for the two names this page happens to use, and would have accused three
|
|
424
|
+
correct tables. So when you check your own schema, write the check from **what your code stores**,
|
|
425
|
+
not from a list of column names borrowed from someone else's — a coarse check has no false
|
|
426
|
+
positives to excuse, it has a pattern to derive. The question each row must be able to answer is
|
|
427
|
+
*which document was this written for*; the key that answers it is yours to name.
|
|
428
|
+
|
|
429
|
+
**A database error carries its status as a number, not inside its message.** Set `statusCode` (or
|
|
430
|
+
`status`) on whatever `db.request` throws. Both contexts shipped here already do; a host that
|
|
431
|
+
implements the seam itself may not, and the player then has to guess from the text.
|
|
432
|
+
|
|
433
|
+
Guessing was the state until 24/08, at six call sites: `message.includes("409")` — the digits
|
|
434
|
+
anywhere in the string. But the message carries the **path**, so it carries the slug, the id, the
|
|
435
|
+
page number. Measured on the real shapes:
|
|
436
|
+
|
|
437
|
+
```
|
|
438
|
+
Supabase POST /doc_presentation_attendees → 409 conflict ✅
|
|
439
|
+
Supabase POST /doc_presentation_attendees?slug=eq.demo409 → 500 conflict ❌
|
|
440
|
+
Supabase PATCH /doc_bot_sessions?id=eq.sess-409abc → 500 conflict ❌
|
|
441
|
+
Supabase GET /doc_pages?page=eq.409 → 503 conflict ❌
|
|
442
|
+
```
|
|
443
|
+
|
|
444
|
+
Three in four. And every site reads `if (!conflict) throw`, so a genuine 500 was **swallowed** and
|
|
445
|
+
the code carried on as though the row already existed. One document whose slug contains `409` —
|
|
446
|
+
a reference number, a date — was enough.
|
|
447
|
+
|
|
448
|
+
The fallback that remains accepts `409` only **after the arrow**, where a status lives and a slug
|
|
449
|
+
cannot. Setting the number spares you that reasoning entirely.
|
|
450
|
+
|
|
401
451
|
**Configured is not served.** When a diagnosis is disputed, the useful question is not who is right
|
|
402
452
|
but *did you measure exactly what fails*. Two true statements about the same instance can describe
|
|
403
453
|
different responses.
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "discovery-media-player",
|
|
3
|
-
"version": "0.1.
|
|
3
|
+
"version": "0.1.134",
|
|
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",
|
|
@@ -0,0 +1,58 @@
|
|
|
1
|
+
// LE FAIT, PAS LE LIBELLÉ — COMMENT ON RECONNAÎT UN CONFLIT D'UNICITÉ.
|
|
2
|
+
//
|
|
3
|
+
// ⚠️ CE QUE FAISAIENT LES SIX SITES, ET CE QUE ÇA COÛTAIT.
|
|
4
|
+
//
|
|
5
|
+
// if (!String((erreur && erreur.message) || "").includes("409")) throw erreur;
|
|
6
|
+
//
|
|
7
|
+
// Le message que `context/standalone.js` compose est `Supabase POST /chemin?… → 409`, et le CHEMIN
|
|
8
|
+
// contient le slug, l'identifiant, le numéro de page. Chercher « 409 » n'importe où dans cette
|
|
9
|
+
// chaîne confond donc le statut avec les DONNÉES. Mesuré le 24/08 sur les formes réelles :
|
|
10
|
+
//
|
|
11
|
+
// Supabase POST /doc_presentation_attendees → 409 conflit ✅
|
|
12
|
+
// Supabase POST /doc_presentation_attendees?slug=eq.demo409 → 500 conflit ❌
|
|
13
|
+
// Supabase PATCH /doc_bot_sessions?id=eq.sess-409abc → 500 conflit ❌
|
|
14
|
+
// Supabase GET /doc_pages?page=eq.409 → 503 conflit ❌
|
|
15
|
+
//
|
|
16
|
+
// Trois sur quatre. Et l'usage est toujours `if (!conflit) throw` : une vraie panne 500 était donc
|
|
17
|
+
// AVALÉE, et le code continuait comme si la ligne existait déjà. Il suffit d'un document dont le
|
|
18
|
+
// slug porte « 409 » — un numéro de référence, une date — pour qu'une erreur de base disparaisse.
|
|
19
|
+
//
|
|
20
|
+
// ⚠️ LE FAIT EXISTE DÉJÀ. `context/standalone.js` pose `erreur.statusCode = r.status` depuis la
|
|
21
|
+
// correction de PGRST202, et son commentaire dit que le studio expose la même chose : « les deux
|
|
22
|
+
// formes convergent enfin ». Six sites lisaient encore le texte à côté.
|
|
23
|
+
//
|
|
24
|
+
// La leçon vient d'un exploitant, qui l'a payée sur son propre outillage : un tri sur un LIBELLÉ
|
|
25
|
+
// s'est renversé le jour où il a trouvé un meilleur mot. Un test sur un libellé a la même
|
|
26
|
+
// fragilité, en pire — il ne se renverse pas, il se trompe en silence.
|
|
27
|
+
//
|
|
28
|
+
// ⚠️ POURQUOI IL RESTE UN REPLI TEXTUEL. Un hôte tiers implémente `db.request` lui-même et peut
|
|
29
|
+
// n'avoir jamais posé `statusCode`. Le repli existe donc, mais il ne cherche plus « 409 » n'importe
|
|
30
|
+
// où : il ne l'accepte QU'À LA PLACE DU STATUT, en fin de message, là où le contrat le met. Un slug
|
|
31
|
+
// ne peut pas s'y trouver.
|
|
32
|
+
|
|
33
|
+
/** Le statut HTTP d'une erreur de base, ou `null` si personne ne l'a posé. */
|
|
34
|
+
const statutDe = (erreur) => {
|
|
35
|
+
const s = erreur && (erreur.statusCode ?? erreur.status);
|
|
36
|
+
return Number.isInteger(s) ? s : null;
|
|
37
|
+
};
|
|
38
|
+
|
|
39
|
+
/**
|
|
40
|
+
* Vrai si cette erreur EST un conflit d'unicité (409).
|
|
41
|
+
*
|
|
42
|
+
* ⚠️ L'ordre compte : le fait d'abord, le texte seulement à défaut. Inverser reviendrait à
|
|
43
|
+
* consulter le libellé même quand le statut est là — c'est-à-dire à garder le défaut avec une
|
|
44
|
+
* façade.
|
|
45
|
+
*/
|
|
46
|
+
function estConflit(erreur) {
|
|
47
|
+
const statut = statutDe(erreur);
|
|
48
|
+
if (statut !== null) return statut === 409;
|
|
49
|
+
// ⚠️ REPLI : « 409 » EN POSITION DE STATUT, c'est-à-dire APRÈS la flèche — jamais ailleurs.
|
|
50
|
+
// Un premier essai exigeait la fin de message ou un tiret de détail : c'était se caler sur le
|
|
51
|
+
// format d'UN contexte, et quatre bancs l'ont refusé en lançant « → 409 duplicate key … ». Ce
|
|
52
|
+
// qu'on peut affirmer sans connaître l'hôte, c'est la POSITION : ce qui suit la flèche est le
|
|
53
|
+
// statut. Un slug, lui, vit dans le chemin — avant elle. C'est ce qui distingue ce repli du
|
|
54
|
+
// défaut qu'il remplace : « 409 » n'importe où, contre « 409 » là où le statut se trouve.
|
|
55
|
+
return /→\s*409\b/.test(String((erreur && erreur.message) || ""));
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
module.exports = { estConflit };
|
package/server/presentations.js
CHANGED
|
@@ -589,7 +589,7 @@ async function addMessage(slug, { name, email, avatar, isPresenter, isMember, bo
|
|
|
589
589
|
// attendu n'est pas un incident — mais rien ne distinguait « je sais ce que je rattrape » de
|
|
590
590
|
// « j'avale tout ». On le dit donc : ce qui n'est pas le conflit attendu remonte, et le conflit
|
|
591
591
|
// attendu est journalisé une fois, en clair, parce qu'un renvoi fréquent est une information.
|
|
592
|
-
const conflit = cle &&
|
|
592
|
+
const conflit = cle && estConflit(erreur);
|
|
593
593
|
if (!conflit) throw erreur;
|
|
594
594
|
try { PLAYER.errors.capture(new Error("message déjà enregistré (renvoi) : " + String(slug)), { route: "present-chat", benin: true }); } catch { /* jamais bloquant */ }
|
|
595
595
|
const deja = await PLAYER.db.request(
|
|
@@ -1163,7 +1163,7 @@ async function recordAttendance(slug, participant, { presentation = null, ipHash
|
|
|
1163
1163
|
// simultanés (deux onglets ouverts ensemble), et le second recevait un 409 que personne ne
|
|
1164
1164
|
// rattrapait — un 500 pour un battement, bénin mais faux. Le conflit dit « la ligne existe
|
|
1165
1165
|
// maintenant » : on la relit et on continue en mise à jour. Tout autre échec remonte.
|
|
1166
|
-
if (!
|
|
1166
|
+
if (!estConflit(erreur)) throw erreur;
|
|
1167
1167
|
// Journalisé comme bénin : deux onglets qui arrivent ensemble sont une information, pas
|
|
1168
1168
|
// une panne — et la garde des écritures muettes exige que tout rattrapage parle.
|
|
1169
1169
|
try { PLAYER.errors.capture(new Error("présence déjà ouverte (second onglet) : " + String(slug)), { route: "present-attend", benin: true }); } catch { /* jamais bloquant */ }
|
|
@@ -1284,6 +1284,7 @@ async function switchPresentationDoc(slug, email, isAdmin, { fileUrl, fileName,
|
|
|
1284
1284
|
// Il traverse trois frontières (présentateur → serveur → audience) : deux implémentations
|
|
1285
1285
|
// finissaient par diverger, et une audience qui ne voit pas la bonne carte n'émet aucune erreur.
|
|
1286
1286
|
const { sanitizeContent } = require("./shared.generated.js");
|
|
1287
|
+
const { estConflit } = require("./erreurs-base.js");
|
|
1287
1288
|
|
|
1288
1289
|
/**
|
|
1289
1290
|
* Une présentation close ne se pilote plus par le chemin PROPRIÉTAIRE.
|
package/server/retention.js
CHANGED
|
@@ -124,6 +124,30 @@ async function retirerFichier(chemin, opts) {
|
|
|
124
124
|
// au RESTE du plafond (`min(taille, plafond - examinees)`) pour ne jamais le dépasser. Et on
|
|
125
125
|
// compte les lignes RENDUES par le DELETE (`return=representation&select=id`), pas les ids
|
|
126
126
|
// présélectionnés : deux exécutions concurrentes n'annoncent pas deux fois la même suppression.
|
|
127
|
+
/**
|
|
128
|
+
* « Cette table n'existe pas ici » — et RIEN d'autre.
|
|
129
|
+
*
|
|
130
|
+
* ⚠️ UNE CIBLE DE PURGE PEUT LÉGITIMEMENT MANQUER, LE CONTRAT LE DIT. `docs/HOST-CONTRACT.md` :
|
|
131
|
+
* les migrations de débit « ne sont délibérément pas » dans le périmètre de la carte, parce
|
|
132
|
+
* qu'« un hôte peut fournir sa propre capacité `limits`, et sur un tel hôte leur absence est
|
|
133
|
+
* NORMALE, pas un défaut ». Le balayage, lui, purgeait `player_rate_limits` sans condition — donc
|
|
134
|
+
* sur cet hôte-là il levait à la cinquième cible sur huit, et les liens révoqués comme les
|
|
135
|
+
* présentations n'étaient JAMAIS atteints. Enveloppé dans un `catch` qui le classe bénin, il
|
|
136
|
+
* restait armé, silencieux, et partiellement inopérant : le pire des trois états.
|
|
137
|
+
*
|
|
138
|
+
* ⚠️ AUSSI ÉTROIT QUE `signatureAbsente`, ET POUR LA MÊME RAISON. On accepte le code PostgREST de la
|
|
139
|
+
* table introuvable, ou son message — et rien d'autre. Un 404 seul ne suffit pas : il peut venir
|
|
140
|
+
* d'ailleurs, et avaler une vraie panne ferait de ce correctif une purge qui ne purge plus sans le
|
|
141
|
+
* dire, c'est-à-dire exactement ce qu'il corrige.
|
|
142
|
+
*/
|
|
143
|
+
function tableAbsente(erreur) {
|
|
144
|
+
if (!erreur) return false;
|
|
145
|
+
const code = erreur.details && (erreur.details.code || (erreur.details.error && erreur.details.error.code));
|
|
146
|
+
if (code === "PGRST205") return true;
|
|
147
|
+
const texte = String(erreur.message || "");
|
|
148
|
+
return texte.includes("PGRST205") || /Could not find the table/i.test(texte);
|
|
149
|
+
}
|
|
150
|
+
|
|
127
151
|
async function purgerParLots(table, filtre, colId, { dryRun = false, taille = LOT, plafond = PLAFOND } = {}, plafondForce) {
|
|
128
152
|
if (plafondForce !== undefined) plafond = plafondForce;
|
|
129
153
|
let examinees = 0, supprimees = 0, tronque = false, curseur = null;
|
|
@@ -200,7 +224,15 @@ async function purgerRetention(now, optsBrutes = {}) {
|
|
|
200
224
|
rapport.commercial_doc_sessions = await purgerParLots("commercial_doc_sessions", `last_at=lt.${enc(bJournaux)}`, "session_id", opts);
|
|
201
225
|
rapport.commercial_doc_internal_sessions = await purgerParLots("commercial_doc_internal_sessions", `last_at=lt.${enc(bJournaux)}`, "session_id", opts);
|
|
202
226
|
rapport.doc_bot_sessions = await purgerParLots("doc_bot_sessions", `last_at=lt.${enc(bJournaux)}`, "id", opts);
|
|
203
|
-
|
|
227
|
+
// ⚠️ LA SEULE CIBLE QUE LE CONTRAT REND OPTIONNELLE — et le balayage la traitait comme les autres.
|
|
228
|
+
// Son absence est normale sur un hôte qui fournit sa propre capacité `limits` ; elle ne doit donc
|
|
229
|
+
// pas interrompre les trois cibles qui suivent. Sautée, ET DITE : un rapport qui omettrait la
|
|
230
|
+
// ligne se lirait comme « rien à supprimer », ce qui est une autre affirmation.
|
|
231
|
+
rapport.player_rate_limits = await purgerParLots("player_rate_limits", `expires_at=lt.${enc(new Date(now).toISOString())}`, "key", opts)
|
|
232
|
+
.catch((e) => {
|
|
233
|
+
if (!tableAbsente(e)) throw e;
|
|
234
|
+
return { examinees: 0, supprimees: 0, tronque: false, sautee: "table absente — l'hôte fournit ses propres compteurs de débit" };
|
|
235
|
+
});
|
|
204
236
|
|
|
205
237
|
// Liens révoqués : seulement là où la révocation est DATÉE (0013).
|
|
206
238
|
const dateDispo = await require("./schema").attendue("revocationDatee");
|
package/server/routes-agent.js
CHANGED
|
@@ -12,6 +12,16 @@ const init = (ctx) => { PLAYER = ctx; docbot = ctx.plugins.bot; };
|
|
|
12
12
|
// chute au bout rend `false` et le dispatch continue. Aucune liste d'actions n'est dupliquée
|
|
13
13
|
// entre ici et handler (un correctif à deux exemplaires finit par diverger) — et aucun appui
|
|
14
14
|
// sur res.writableEnded, absent des `res` postiches des bancs comme de certains hôtes.
|
|
15
|
+
|
|
16
|
+
/**
|
|
17
|
+
* Les actions qui AGISSENT SUR UNE SESSION EXISTANTE — c'est-à-dire toutes celles du bloc assistant
|
|
18
|
+
* sauf `bot-start`, qui la crée. La liste vit ici plutôt que dans chaque branche pour que le banc
|
|
19
|
+
* puisse la CONFRONTER à l'aiguillage : une action ajoutée à l'un et pas à l'autre fait rougir.
|
|
20
|
+
*/
|
|
21
|
+
const ACTIONS_LIEES_A_UNE_SESSION = new Set([
|
|
22
|
+
"bot-say", "bot-history", "bot-nudge", "bot-book", "bot-contact", "bot-rate", "bot-script",
|
|
23
|
+
]);
|
|
24
|
+
|
|
15
25
|
async function traiter(req, res, body, _slug) {
|
|
16
26
|
if (body.action === "bot-tts") {
|
|
17
27
|
const jp = (status, obj) => { res.statusCode = status; res.setHeader("Content-Type", "application/json"); res.end(JSON.stringify(obj)); };
|
|
@@ -101,11 +111,38 @@ async function traiter(req, res, body, _slug) {
|
|
|
101
111
|
if (!allowed) return jp(429, { ok: false, error: "rate" });
|
|
102
112
|
const share = await getShareBySlug(String(body.slug || ""));
|
|
103
113
|
if (!share || !share.bot_enabled) return jp(404, { ok: false, error: "bot" });
|
|
114
|
+
// ⚠️ UNE SESSION EST LIÉE À SON DOCUMENT — VÉRIFIÉ ICI, POUR TOUTES LES ACTIONS À LA FOIS.
|
|
115
|
+
//
|
|
116
|
+
// Cette vérification vivait DANS chaque action, et trois seulement sur sept la faisaient :
|
|
117
|
+
// `bot-rate`, `bot-script` et `bot-history` (celle-ci ajoutée après coup, en 0.1.131).
|
|
118
|
+
// `bot-say`, `bot-nudge`, `bot-book` et `bot-contact` prenaient le même `sessionId` venu du
|
|
119
|
+
// client sans jamais le rattacher au document demandé.
|
|
120
|
+
//
|
|
121
|
+
// ⚠️ CE QUE ÇA OUVRAIT EST PIRE QUE CE QU'ON VENAIT DE FERMER. `bot-history` LISAIT la
|
|
122
|
+
// conversation d'un autre document ; `bot-say` y ÉCRIT et se fait répondre, `bot-contact`
|
|
123
|
+
// attache un nom, un e-mail et un téléphone à la session d'un autre client. Une porte en
|
|
124
|
+
// lecture et quatre en écriture, dans le même bloc.
|
|
125
|
+
//
|
|
126
|
+
// ⚠️ ET LA LEÇON N'EST PAS « IL EN MANQUAIT QUATRE ». Écrite sept fois, cette garde serait
|
|
127
|
+
// oubliée une huitième au prochain ajout — le défaut n'est pas la ligne absente, c'est que
|
|
128
|
+
// sa présence dépende de la mémoire de celui qui écrit l'action. Elle est donc hissée : une
|
|
129
|
+
// action qui porte une session la prouve, sans que son auteur ait à y penser.
|
|
130
|
+
//
|
|
131
|
+
// Le relevé qui a mené ici vient d'un exploitant : « une vérification que plusieurs actions
|
|
132
|
+
// font chacune pour son compte — il faut compter les ACTIONS, pas les implémentations ».
|
|
133
|
+
// La duplication était homogène, donc invisible à un balayage des variantes ; c'est la
|
|
134
|
+
// COUVERTURE qui divergeait, et rien ne la mesurait. `gardesAgent.test.js` la mesure
|
|
135
|
+
// désormais, en lisant la liste d'actions dans ce fichier même.
|
|
136
|
+
//
|
|
137
|
+
// ⚠️ `bot-start` EST EXCLUE, ET C'EST LA SEULE. Elle CRÉE la session : exiger qu'elle en
|
|
138
|
+
// prouve une fermerait l'assistant à tout le monde.
|
|
139
|
+
if (ACTIONS_LIEES_A_UNE_SESSION.has(body.action)) {
|
|
140
|
+
const liee = await docbot.getSession(String(body.sessionId || ""));
|
|
141
|
+
if (!liee || liee.share_slug !== share.slug) return jp(400, { ok: false, error: "session" });
|
|
142
|
+
}
|
|
104
143
|
const pages = Math.max(0, Math.min(500, Number(body.pages) || 0));
|
|
105
144
|
const mobile = body.mobile === 1 || body.mobile === true; // téléphone → messages courts + autoplay steps
|
|
106
145
|
if (body.action === "bot-rate") { // satisfaction (1-5 étoiles) posée depuis le bloc central du viewer
|
|
107
|
-
const sess = await docbot.getSession(String(body.sessionId || ""));
|
|
108
|
-
if (!sess || sess.share_slug !== share.slug) return jp(400, { ok: false, error: "session" });
|
|
109
146
|
// ⚠️ CE REFUS ÉTAIT INATTEIGNABLE, ET C'EST LA MESURE QUI EN PAYAIT LE PRIX.
|
|
110
147
|
//
|
|
111
148
|
// const note = Math.max(1, Math.min(5, Number(body.rating) || 0));
|
|
@@ -155,8 +192,6 @@ async function traiter(req, res, body, _slug) {
|
|
|
155
192
|
//
|
|
156
193
|
// Les deux voisines avaient la garde ET leur banc ; celle-ci n'avait ni l'une ni
|
|
157
194
|
// l'autre, et la relecture qui a ajouté les bancs a couvert deux actions sur trois.
|
|
158
|
-
const sess = await docbot.getSession(String(body.sessionId || ""));
|
|
159
|
-
if (!sess || sess.share_slug !== share.slug) return jp(400, { ok: false, error: "session" });
|
|
160
195
|
return jp(200, { ok: true, messages: await docbot.listMessages(String(body.sessionId || "")) });
|
|
161
196
|
}
|
|
162
197
|
// ⚠️ Même piège : un objet littéral répond à `constructor`. Sans `Object.hasOwn`, une
|
|
@@ -168,8 +203,6 @@ async function traiter(req, res, body, _slug) {
|
|
|
168
203
|
// Bascule de langue EN COURS de présentation : renvoie le script (traduit ou FR) — le client
|
|
169
204
|
// remplace sa liste d'étapes et rejoue le message courant dans la nouvelle langue.
|
|
170
205
|
if (body.action === "bot-script") {
|
|
171
|
-
const sess = await docbot.getSession(String(body.sessionId || ""));
|
|
172
|
-
if (!sess || sess.share_slug !== share.slug) return jp(400, { ok: false, error: "session" });
|
|
173
206
|
const sp = docbot.applyPron(await docbot.scriptedPayload(share.doc_id, blang, String(body.sessionId)), await docbot.getProfile(share.bot_profile_id));
|
|
174
207
|
if (!sp) return jp(400, { ok: false, error: "script" });
|
|
175
208
|
return jp(200, { ok: true, steps: sp.steps, voiceScript: sp.voice, message: sp.hook, closing: sp.closing, messageSay: sp.hookSay, closingSay: sp.closingSay });
|
package/server/routes-liens.js
CHANGED
|
@@ -2,6 +2,7 @@
|
|
|
2
2
|
// Reste à PLAT dans server/ (les gardes de forge ciblent server/*.js).
|
|
3
3
|
|
|
4
4
|
const { adresseAppelant } = require("./appelant");
|
|
5
|
+
const { estConflit } = require("./erreurs-base.js");
|
|
5
6
|
const { createShare, createReshare, sendReshareEmail, revokeShare, setShareAuth, listSharesForDoc, listSessionsForDoc, internalStatsForDoc, cleIdempotence, getShareBySlug, logView, upsertSession, upsertInternalSession, overview: docOverview } = require("./shares");
|
|
6
7
|
const { SESSION_QUOTA_PER_HOUR, VIEW_QUOTA_PER_HOUR } = require("./shared.generated.js");
|
|
7
8
|
|
|
@@ -142,7 +143,7 @@ async function traiter(req, res, body, slug) {
|
|
|
142
143
|
body: { doc_title: body.docTitle || null, file_url: String(body.fileUrl), file_name: body.fileName || null, revoked: false, ...(cleDispo ? { idem_key: cleHote } : {}) },
|
|
143
144
|
});
|
|
144
145
|
} catch (erreur) {
|
|
145
|
-
if (!
|
|
146
|
+
if (!estConflit(erreur)) throw erreur;
|
|
146
147
|
try { PLAYER.errors.capture(new Error("backfill hôte : la clé était déjà posée ailleurs — " + docId), { route: "hostshare", benin: true }); } catch { /* jamais bloquant */ }
|
|
147
148
|
const gagnant = await PLAYER.db.request(`commercial_doc_shares?idem_key=eq.${encodeURIComponent(cleHote)}&select=slug&limit=1`);
|
|
148
149
|
if (!Array.isArray(gagnant) || !gagnant[0]) throw erreur;
|
|
@@ -169,7 +170,7 @@ async function traiter(req, res, body, slug) {
|
|
|
169
170
|
});
|
|
170
171
|
return jd(200, { ok: true, slug: neuf.slug, reused: false });
|
|
171
172
|
} catch (erreur) {
|
|
172
|
-
if (!
|
|
173
|
+
if (!estConflit(erreur)) throw erreur;
|
|
173
174
|
try { PLAYER.errors.capture(new Error("lien hôte déjà créé par une demande simultanée : " + docId), { route: "hostshare", benin: true }); } catch { /* jamais bloquant */ }
|
|
174
175
|
const gagnant = await PLAYER.db.request(`commercial_doc_shares?idem_key=eq.${encodeURIComponent(cleHote)}&select=slug&limit=1`);
|
|
175
176
|
if (!Array.isArray(gagnant) || !gagnant[0]) throw erreur; // 409 d'autre chose : on ne l'invente pas
|
|
@@ -219,7 +220,7 @@ async function traiter(req, res, body, slug) {
|
|
|
219
220
|
try {
|
|
220
221
|
await PLAYER.db.request(`commercial_doc_shares?slug=eq.${encodeURIComponent(ex[0].slug)}`, { method: "PATCH", headers: { Prefer: "return=minimal" }, body: { doc_title: body.docTitle || null, file_url: String(body.fileUrl), file_name: body.fileName || null, bot_enabled: true, bot_guided: true, bot_profile_id: (body.profileId || "").trim() || null, revoked: false, ...(cleDispo ? { idem_key: cleTest } : {}) } });
|
|
221
222
|
} catch (erreur) {
|
|
222
|
-
if (!
|
|
223
|
+
if (!estConflit(erreur)) throw erreur;
|
|
223
224
|
try { PLAYER.errors.capture(new Error("backfill répétition : la clé était déjà posée ailleurs — " + docId), { route: "docshare-test", benin: true }); } catch { /* jamais bloquant */ }
|
|
224
225
|
const gagnant = await PLAYER.db.request(`commercial_doc_shares?idem_key=eq.${encodeURIComponent(cleTest)}&select=slug&limit=1`);
|
|
225
226
|
if (!Array.isArray(gagnant) || !gagnant[0]) throw erreur;
|
|
@@ -232,7 +233,7 @@ async function traiter(req, res, body, slug) {
|
|
|
232
233
|
const t = await createShare({ docId, docTitle: body.docTitle, fileUrl: body.fileUrl, fileName: body.fileName, recipientName: "Répétition (test)", createdBy: u.email, bot: true, guided: true, profileId: body.profileId, isTest: true, idemKey: cleTest });
|
|
233
234
|
return jd(200, { ok: true, slug: t.slug });
|
|
234
235
|
} catch (erreur) {
|
|
235
|
-
if (!
|
|
236
|
+
if (!estConflit(erreur)) throw erreur;
|
|
236
237
|
try { PLAYER.errors.capture(new Error("lien de répétition déjà créé par une demande simultanée : " + docId), { route: "docshare-test", benin: true }); } catch { /* jamais bloquant */ }
|
|
237
238
|
const gagnant = await PLAYER.db.request(`commercial_doc_shares?idem_key=eq.${encodeURIComponent(cleTest)}&select=slug&limit=1`);
|
|
238
239
|
if (!Array.isArray(gagnant) || !gagnant[0]) throw erreur;
|