discovery-media-player 0.1.60 → 0.1.61
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 +19 -0
- package/package.json +1 -1
- package/server/handler.js +36 -2
- package/server/presentations.js +22 -0
package/docs/HOST-CONTRACT.md
CHANGED
|
@@ -65,6 +65,25 @@ it. `verdict` is then one of:
|
|
|
65
65
|
| `partiel` | some expectations checked, none of them missing |
|
|
66
66
|
| `complet` | all checked, all present |
|
|
67
67
|
| `incomplet` | at least one is missing — `manquant` names the file and the sleeping feature |
|
|
68
|
+
|
|
69
|
+
Each `manquant` entry has **exactly this shape** — pin your parser to it, not to what a schema
|
|
70
|
+
probe "should" return:
|
|
71
|
+
|
|
72
|
+
```json
|
|
73
|
+
{ "migration": "supabase/migrations/0006-reactions-ordonnees.sql",
|
|
74
|
+
"fonction": "empêcher deux réactions simultanées de s'écraser" }
|
|
75
|
+
```
|
|
76
|
+
|
|
77
|
+
⚠️ This shape went **undocumented** for two releases, and the second host typed it from memory as
|
|
78
|
+
`{table, colonne, migration}` — their monitoring filter then discarded every real entry and would
|
|
79
|
+
have shown an empty table on a database that was actually missing something. **No mutation on
|
|
80
|
+
their side could catch it: their test built the card the same wrong way, so the bench validated
|
|
81
|
+
the assumption instead of the behaviour.** The only cure was reading data they had not fabricated.
|
|
82
|
+
If you consume this card, test your parser against the JSON above, not against a fixture you wrote.
|
|
83
|
+
|
|
84
|
+
⚠️ **A card without a `schema` field is an alert, not a success**: it signals an instance older
|
|
85
|
+
than 0.1.58 — a version that cannot answer the question. (Rule contributed by the second host, for
|
|
86
|
+
exactly the monitoring case where "no data" would otherwise read as "all clear".)
|
|
68
87
|
| `indetermine` | the database did not answer; this measurement did not happen |
|
|
69
88
|
|
|
70
89
|
⚠️ **`incomplet` wins over `partiel`**: a missing column is a positive fact and settles the verdict
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "discovery-media-player",
|
|
3
|
-
"version": "0.1.
|
|
3
|
+
"version": "0.1.61",
|
|
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
|
@@ -2102,6 +2102,8 @@ ${LEGAL_CSS}
|
|
|
2102
2102
|
.pbar-link{border:1px solid #e4e0d9;border-radius:8px;padding:6px 9px;font:inherit;font-size:12px;color:#555;width:240px;max-width:32vw;background:#f8f6f2}
|
|
2103
2103
|
.pbar-btn{border:1px solid #e4e0d9;background:#f6f4ef;color:#1c1c1c;border-radius:8px;padding:7px 11px;font:inherit;font-size:12.5px;font-weight:600;cursor:pointer;white-space:nowrap}
|
|
2104
2104
|
.pbar-btn:hover{background:#eceae5}
|
|
2105
|
+
.pbar-err{display:none;color:#b3261e;font-size:12px;font-weight:600;max-width:260px;line-height:1.25}
|
|
2106
|
+
.pbar-err.on{display:inline-block}
|
|
2105
2107
|
#pbarEnd{color:#c0392b}
|
|
2106
2108
|
/* Barre d'outils responsive : sous 860px, zoom/plein écran/Présenter/Partager/Télécharger passent dans le
|
|
2107
2109
|
menu « ⋯ » ; le titre se tronque puis disparaît. */
|
|
@@ -2149,6 +2151,7 @@ ${LEGAL_CSS}
|
|
|
2149
2151
|
<button class=pbar-btn id=pbarInvite>Inviter l'équipe</button>
|
|
2150
2152
|
<button class=pbar-btn id=pbarHandover>Passer la main</button>
|
|
2151
2153
|
<button class=pbar-btn id=pbarEnd>Terminer</button>
|
|
2154
|
+
<span class=pbar-err id=pbarErr role=alert></span>
|
|
2152
2155
|
</div>` : ""}
|
|
2153
2156
|
<div class=pop id=pop>
|
|
2154
2157
|
<h4>Transmettre le document</h4>
|
|
@@ -2348,15 +2351,39 @@ ${LEGAL_CSS}
|
|
|
2348
2351
|
if(!PRES) return Promise.resolve();
|
|
2349
2352
|
var p=PRES, btn=document.getElementById('pbarEnd'), libelle=btn?btn.textContent:'';
|
|
2350
2353
|
if(btn){ btn.disabled=true; btn.textContent='…'; }
|
|
2354
|
+
var errAvant=document.getElementById('pbarErr'); if(errAvant){ errAvant.classList.remove('on'); }
|
|
2351
2355
|
return Player.live.fetchBorne('/api/doc',{method:'POST',headers:{'Content-Type':'application/json'},keepalive:true,
|
|
2352
2356
|
body:JSON.stringify({action:'present-end',slug:p.slug,control:p.control})})
|
|
2353
2357
|
.then(function(r){ if(!r||!r.ok) throw new Error('present-end'); })
|
|
2354
2358
|
.then(function(){
|
|
2359
|
+
// RELIRE, PAS REUTILISER : un second aller-retour, indépendant de l'écriture. Réserve
|
|
2360
|
+
// posée par le second hôte, et elle est juste — relire la réponse du PATCH ferait ce que
|
|
2361
|
+
// faisait le cache négatif : la mesure confirmerait ce que l'écriture CROIT avoir fait.
|
|
2362
|
+
// C'est le seul moyen de rendre observable la classe entière des succès silencieux, y
|
|
2363
|
+
// compris contre un futur serveur qui répondrait ok sans avoir écrit.
|
|
2364
|
+
return Player.live.fetchBorne('/api/doc?present='+encodeURIComponent(p.slug)+'&state=1')
|
|
2365
|
+
.then(function(r){ return r&&r.ok?r.json():null; })
|
|
2366
|
+
.catch(function(){ return null; }); // relecture INDISPONIBLE n'est pas « encore active »
|
|
2367
|
+
})
|
|
2368
|
+
.then(function(d){
|
|
2369
|
+
if(d&&d.state&&d.state.active){
|
|
2370
|
+
// Le serveur a dit oui, la base dit encore active : fin non prise, ou reprise ailleurs.
|
|
2371
|
+
// Dans les deux cas, fermer l'interface mentirait au présentateur.
|
|
2372
|
+
if(btn){ btn.disabled=false; btn.textContent=libelle||'Terminer'; }
|
|
2373
|
+
var err2=document.getElementById('pbarErr');
|
|
2374
|
+
if(err2){ err2.textContent='La présentation est ENCORE ACTIVE après la fin — reprise ailleurs, ou fin non prise. Réessayez.'; err2.classList.add('on'); }
|
|
2375
|
+
return;
|
|
2376
|
+
}
|
|
2355
2377
|
PRES=null; clearInterval(_hbIv); clearCtl(p.slug);
|
|
2356
2378
|
var pb=document.getElementById('pbar'); if(pb)pb.style.display='none';
|
|
2357
2379
|
try{ if(window.Live){ Live.sendState(); Live.disconnect(); } }catch(e){}
|
|
2358
2380
|
}, function(){
|
|
2359
|
-
|
|
2381
|
+
// L'échec d'une clôture ne prive de rien qu'on regarde : son succès ne produit rien,
|
|
2382
|
+
// donc son échec non plus. La première version le disait dans un TITLE — un tooltip que
|
|
2383
|
+
// personne ne survole. Le second hôte a eu des présentations « actives » trois jours.
|
|
2384
|
+
if(btn){ btn.disabled=false; btn.textContent=libelle||'Terminer'; }
|
|
2385
|
+
var err=document.getElementById('pbarErr');
|
|
2386
|
+
if(err){ err.textContent='La présentation est TOUJOURS ACTIVE — la fin n’a pas été enregistrée. Réessayez.'; err.classList.add('on'); }
|
|
2360
2387
|
});
|
|
2361
2388
|
}
|
|
2362
2389
|
// ⚠️ LE SEUL ENDROIT OÙ « sendBeacon » A UN SENS : la page s'en va, aucune réponse ne pourra être
|
|
@@ -3107,7 +3134,14 @@ async function handler(req, res) {
|
|
|
3107
3134
|
? await touchPresentation(String(body.slug || ""), String(body.control || ""))
|
|
3108
3135
|
: await endPresentation(String(body.slug || ""), String(body.control || ""));
|
|
3109
3136
|
return jp(r.ok ? 200 : (r.status || 400), r);
|
|
3110
|
-
} catch
|
|
3137
|
+
} catch (erreur) {
|
|
3138
|
+
// ⚠️ CE CATCH A AVALÉ TROIS JOURS DE CLÔTURES IMPOSSIBLES SANS UNE TRACE. Chez le second
|
|
3139
|
+
// hôte, chaque « Terminer » échouait en 23502 (marqueur d'archive NOT NULL) — et ce 500
|
|
3140
|
+
// muet ne laissait RIEN, même pas une ligne dans le journal d'erreurs. Un journal que
|
|
3141
|
+
// personne ne lit vaut peu ; aucun journal ne vaut rien du tout.
|
|
3142
|
+
try { PLAYER.errors.capture(erreur instanceof Error ? erreur : new Error(String(erreur)), { route: "present-" + String(body.action || "").replace(/^present-/, "") }); } catch { /* jamais bloquant */ }
|
|
3143
|
+
return jp(500, { ok: false });
|
|
3144
|
+
}
|
|
3111
3145
|
}
|
|
3112
3146
|
// Assistant IA « présentateur » (bot) sur un lien tracé bot_enabled. PUBLIC (prospect anonyme) → rate-limit IP.
|
|
3113
3147
|
// Synthèse vocale (ElevenLabs) : Léa lit ses messages. PUBLIC (audience anonyme), mais gated : la clé
|
package/server/presentations.js
CHANGED
|
@@ -24,8 +24,29 @@ const tokenMatches = (token, storedHash) => !!(token && storedHash && sameHash(s
|
|
|
24
24
|
// owner = { id, email, name, avatar } (membre authentifié). La CLÉ de propriété est l'EMAIL (issu du JWT
|
|
25
25
|
// vérifié) → permet reprise / liste / transfert, y compris vers un membre choisi par email.
|
|
26
26
|
const lc = (s) => String(s || "").trim().toLowerCase();
|
|
27
|
+
/**
|
|
28
|
+
* ⚠️ LA PURGE NE DOIT PAS DÉPENDRE DE L'OUVERTURE D'UN PANNEAU. Elle ne vivait que dans
|
|
29
|
+
* `listActivePresentations` — c'est-à-dire qu'elle ne tournait que si quelqu'un ouvrait la liste.
|
|
30
|
+
* Le second hôte a eu QUATRE présentations bloquées « actives » pendant TROIS JOURS : personne
|
|
31
|
+
* n'avait ouvert le panneau, donc personne n'avait déclenché le seul mécanisme qui les fermait.
|
|
32
|
+
* Un échec de clôture ne prive de rien qu'on regarde — la purge doit donc s'accrocher à un geste
|
|
33
|
+
* qui arrive de lui-même. Démarrer une présentation en est un : ce sont les présentateurs qui
|
|
34
|
+
* créent les orphelines, et le suivant nettoie celles d'avant.
|
|
35
|
+
*
|
|
36
|
+
* Conditionnée à `active=eq.true&last_seen=lte.seuil` : une session qui vient de battre n'est pas
|
|
37
|
+
* touchée (même garde que la purge du panneau, chantier des écritures conditionnées).
|
|
38
|
+
*/
|
|
39
|
+
async function purgerPerimees(now) {
|
|
40
|
+
const seuil = new Date(now - STALE_MS).toISOString();
|
|
41
|
+
await PLAYER.db.request(
|
|
42
|
+
`doc_presentations?active=eq.true&last_seen=lte.${enc(seuil)}`,
|
|
43
|
+
{ method: "PATCH", headers: { Prefer: "return=minimal" }, body: { active: false, updated_at: new Date(now).toISOString() } },
|
|
44
|
+
).catch(() => { /* la purge est un service, jamais un préalable */ });
|
|
45
|
+
}
|
|
46
|
+
|
|
27
47
|
async function createPresentation({ docId, fileUrl, fileName, docTitle, presenterName, owner }) {
|
|
28
48
|
if (!fileUrl) throw Object.assign(new Error("doc invalide"), { statusCode: 400 });
|
|
49
|
+
await purgerPerimees(Date.now());
|
|
29
50
|
const slug = newToken(9); // ~12 chars URL-safe
|
|
30
51
|
const control = newToken(18); // secret pilotage
|
|
31
52
|
const o = owner && typeof owner === "object" ? owner : {};
|
|
@@ -846,4 +867,5 @@ async function listPresentationsForDoc(docId, email, isAdmin, autoriseLarge) {
|
|
|
846
867
|
|
|
847
868
|
module.exports = {
|
|
848
869
|
reacteurDepuisJeton,
|
|
870
|
+
purgerPerimees,
|
|
849
871
|
messagePublic, CHAMPS_PUBLICS, init, createPresentation, getPresentation, setPage, endPresentation, addMessage, listMessages, toggleReaction, editMessage, deleteMessage, setChatLock, createUploadUrl, reclaimPresentation, touchPresentation, listActivePresentations, handoverPresentation, endPresentationByOwner, recordAttendance, presentationStats, listPresentationsForDoc, switchPresentationDoc, setPresentationContent };
|