discovery-media-player 0.1.59 → 0.1.60
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/context/standalone.js +8 -1
- package/context/storage.js +9 -0
- package/docs/HOST-CONTRACT.md +8 -1
- package/package.json +1 -1
- package/server/browser.generated.js +1 -1
- package/server/handler.js +26 -6
- package/server/presentations.js +100 -21
- package/server/schema.js +59 -2
- package/supabase/init.sql +61 -10
- package/supabase/migrations/0006-reactions-ordonnees.sql +17 -0
- package/supabase/migrations/0007-archive-scellee.sql +49 -0
- package/supabase/migrations/0008-cloture-possible.sql +19 -0
package/context/standalone.js
CHANGED
|
@@ -63,7 +63,14 @@ function creerDb(env) {
|
|
|
63
63
|
},
|
|
64
64
|
body: options.body ? JSON.stringify(options.body) : undefined,
|
|
65
65
|
});
|
|
66
|
-
if (!r.ok)
|
|
66
|
+
if (!r.ok) {
|
|
67
|
+
// ⚠️ LE CORPS DIT POURQUOI, LE CODE NE DIT QUE COMBIEN. Un « 400 » nu a coûté un aller-retour
|
|
68
|
+
// de forge complet pour apprendre ce que PostgREST avait écrit dans sa réponse depuis le
|
|
69
|
+
// début. Tronqué : c'est un message d'erreur, pas un relais de données.
|
|
70
|
+
let detail = "";
|
|
71
|
+
try { detail = (await r.text()).slice(0, 300); } catch { /* le statut restera seul */ }
|
|
72
|
+
throw new Error(`Supabase ${methode} ${chemin} → ${r.status}${detail ? ` — ${detail}` : ""}`);
|
|
73
|
+
}
|
|
67
74
|
const texte = await r.text();
|
|
68
75
|
return texte ? JSON.parse(texte) : null;
|
|
69
76
|
}
|
package/context/storage.js
CHANGED
|
@@ -173,6 +173,15 @@ async function lireDepuis(fh, stat, cible, range) {
|
|
|
173
173
|
statut = 206;
|
|
174
174
|
}
|
|
175
175
|
|
|
176
|
+
// ⚠️ LE PLAFOND AVANT L'ALLOCATION — parce que c'est l'allocation qui coûte. Le contrôle du
|
|
177
|
+
// relais (Content-Length, dans le handler) arrivait APRÈS ce Buffer.alloc : un fichier local plus
|
|
178
|
+
// grand que le plafond était alloué EN ENTIER à chaque requête, puis refusé — le refus payait
|
|
179
|
+
// exactement le prix qu'il devait éviter. Lu à l'appel et non au chargement : le serveur autonome
|
|
180
|
+
// fabrique son contexte à l'import, avant que l'exploitant ait pu poser la variable.
|
|
181
|
+
const plafond = Number(process.env.PLAYER_MAX_RELAY_BYTES || 0) || 60 * 1024 * 1024;
|
|
182
|
+
if (fin - debut + 1 > plafond) {
|
|
183
|
+
return reponse(413, {}, Buffer.alloc(0));
|
|
184
|
+
}
|
|
176
185
|
const buf = Buffer.alloc(fin - debut + 1);
|
|
177
186
|
const { bytesRead } = await fh.read(buf, 0, buf.length, debut);
|
|
178
187
|
const corps = bytesRead === buf.length ? buf : buf.subarray(0, bytesRead);
|
package/docs/HOST-CONTRACT.md
CHANGED
|
@@ -32,7 +32,7 @@ need.
|
|
|
32
32
|
"hostShare": true,
|
|
33
33
|
"hostMail": true,
|
|
34
34
|
"plugins": { "bot": false, "visitors": false, "brandIntro": false, "botBrowser": false, "providerQuotas": false },
|
|
35
|
-
"schema": { "attendues":
|
|
35
|
+
"schema": { "couvre": "colonnes-conditionnelles", "attendues": 4, "sondees": 1, "verdict": "partiel", "manquant": [] }
|
|
36
36
|
}
|
|
37
37
|
```
|
|
38
38
|
|
|
@@ -70,6 +70,13 @@ it. `verdict` is then one of:
|
|
|
70
70
|
⚠️ **`incomplet` wins over `partiel`**: a missing column is a positive fact and settles the verdict
|
|
71
71
|
on its own, even when the rest has not been checked.
|
|
72
72
|
|
|
73
|
+
⚠️ **`couvre` states the scope, because `complet` without a scope overpromises.** The card checks
|
|
74
|
+
the **conditional columns** — the ones a feature probes before writing. The rate-limit migrations
|
|
75
|
+
(`0003`, `0004`) are deliberately **not** in it: a host may provide its own `limits` capability, and
|
|
76
|
+
on such a host their absence is normal, not a defect. `complet` therefore never means *every file
|
|
77
|
+
under `supabase/` has been applied* — read it as *every column this code conditions its writes on
|
|
78
|
+
is present*.
|
|
79
|
+
|
|
73
80
|
⚠️ **`indetermine` is not a failure of the card.** A control column — the primary key of the oldest
|
|
74
81
|
table — is queried first. If *it* does not answer, nothing is missing: the database is. Without that
|
|
75
82
|
control, an unreachable database would make all three probes fail and the card would announce three
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "discovery-media-player",
|
|
3
|
-
"version": "0.1.
|
|
3
|
+
"version": "0.1.60",
|
|
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",
|
|
@@ -4,4 +4,4 @@
|
|
|
4
4
|
// Injecté par api/doc.js dans un <script nonce=…> de la visionneuse. Le code navigateur du
|
|
5
5
|
// player vit en modules TypeScript testés sous player/src/ — c'est ici qu'il atterrit.
|
|
6
6
|
"use strict";
|
|
7
|
-
module.exports = { PLAYER_BROWSER_JS: "\"use strict\";var Player=(()=>{var C=Object.defineProperty,He=Object.defineProperties,De=Object.getOwnPropertyDescriptor,We=Object.getOwnPropertyDescriptors,Be=Object.getOwnPropertyNames,he=Object.getOwnPropertySymbols;var we=Object.prototype.hasOwnProperty,Ve=Object.prototype.propertyIsEnumerable;var ve=(e,n,t)=>n in e?C(e,n,{enumerable:!0,configurable:!0,writable:!0,value:t}):e[n]=t,G=(e,n)=>{for(var t in n||(n={}))we.call(n,t)&&ve(e,t,n[t]);if(he)for(var t of he(n))Ve.call(n,t)&&ve(e,t,n[t]);return e},Me=(e,n)=>He(e,We(n));var v=(e,n)=>{for(var t in n)C(e,t,{get:n[t],enumerable:!0})},ze=(e,n,t,r)=>{if(n&&typeof n==\"object\"||typeof n==\"function\")for(let o of Be(n))!we.call(e,o)&&o!==t&&C(e,o,{get:()=>n[o],enumerable:!(r=De(n,o))||r.enumerable});return e};var Fe=e=>ze(C({},\"__esModule\",{value:!0}),e);var Gn={};v(Gn,{bridge:()=>L,cadence:()=>N,chat:()=>$,live:()=>U,presentation:()=>H,presentationState:()=>W,tracking:()=>O,viewer:()=>D});var L={};v(L,{onHostMessage:()=>en,onPlayerMessage:()=>Je,parseHostMessage:()=>_e,parsePlayerMessage:()=>Te,sendToHost:()=>qe,sendToPlayer:()=>Qe,toWire:()=>Y});var K=\"3dd-doc-\",je=/^[A-Za-z0-9_-]{1,64}$/,Ge=[\"close\",\"share\",\"embed-ready\",\"present-left\",\"present-denied\"],Ke=[\"embed-denied\"],Ye=[\"present-invite\",\"present-handover\",\"present-switch\"],Ze=[\"handover-done\"];function Y(e){let n={type:K+e.type};return\"slug\"in e&&e.slug&&(n.slug=e.slug),\"reason\"in e&&e.reason&&(n.reason=String(e.reason).slice(0,40)),n}function xe(e,n,t){if(!e||typeof e!=\"object\")return null;let r=e.type;if(typeof r!=\"string\"||!r.startsWith(K))return null;let o=r.slice(K.length);if(n.includes(o))return{type:o};if(Ke.includes(o)){let a=e.reason;return{type:o,reason:/^[a-z-]{1,40}$/.test(String(a||\"\"))?String(a):\"unknown\"}}if(!t.includes(o))return null;let s=e.slug;return typeof s!=\"string\"||!je.test(s)?null:{type:o,slug:s}}function Te(e){return xe(e,Ge,Ye)}function _e(e){return xe(e,Ze,[])}function qe(e,n){try{let t=n||(typeof window!=\"undefined\"?window.parent:null);t&&t.postMessage(Y(e),\"*\")}catch(t){}}function Qe(e,n){var t;try{(t=e==null?void 0:e.contentWindow)==null||t.postMessage(Y(n),\"*\")}catch(r){}}function Xe(e,n){var r;if(!e)return!0;let t=(r=e.contentWindow)!=null?r:e;return n===t}function Je(e,n,t){let r=n||(typeof window!=\"undefined\"?window:null);if(!r)return()=>{};let o=s=>{if(!Xe(t,s.source))return;let a=Te(s.data);a&&e(a)};return r.addEventListener(\"message\",o),()=>r.removeEventListener(\"message\",o)}function en(e,n){var s;let t=n||(typeof window!=\"undefined\"?window:null);if(!t)return()=>{};let r=(s=t.parent)!=null?s:null,o=a=>{if(!r||r===t||a.source!==r)return;let u=_e(a.data);u&&e(u)};return t.addEventListener(\"message\",o),()=>t.removeEventListener(\"message\",o)}var O={};v(O,{createTracker:()=>ln});var N={};v(N,{PRESENTER_ACTIONS_PER_HOUR:()=>X,PRESENT_CACHE_MS:()=>rn,PRESENT_QUOTA_MARGIN:()=>Ie,PRESENT_QUOTA_PER_HOUR:()=>on,PRESENT_READS_PER_HOUR:()=>Re,PRESENT_READ_BURST:()=>Pe,PRESENT_READ_COALESCE_MS:()=>Ae,PRESENT_RESYNC_MS:()=>Se,PRESENT_SIGNAL_BUDGET_PER_HOUR:()=>tn,PRESENT_WRITE_TIMEOUT_MS:()=>J,READERS_PER_EGRESS:()=>q,RESYNC_READS_PER_HOUR:()=>ke,SESSION_IDLE_MS:()=>Q,SESSION_INTERVAL_MS:()=>Z,SESSION_QUOTA_PER_HOUR:()=>nn,SESSION_WRITES_PER_HOUR:()=>Ee});var Z=12e3,Ee=Math.ceil(300),q=25,nn=Ee*q,Q=18e4,Se=25e3,ke=Math.ceil(36e5/Se),X=720,Re=ke+X,tn=X,Pe=20,J=1e4,Ae=400,rn=Ae,Ie=4,on=(Re+Pe)*q*Ie;var sn=[\"mousemove\",\"mousedown\",\"keydown\",\"wheel\",\"touchstart\",\"pointerdown\"];function an(){let e=typeof globalThis!=\"undefined\"?globalThis.crypto:void 0;if(e&&typeof e.randomUUID==\"function\")return e.randomUUID().replace(/-/g,\"\");if(e&&typeof e.getRandomValues==\"function\"){let n=e.getRandomValues(new Uint8Array(16));return Array.from(n,t=>t.toString(16).padStart(2,\"0\")).join(\"\")}return Math.random().toString(36).slice(2)+Date.now().toString(36)}function un(e,n){return t=>{try{let r=JSON.stringify(t),o=n.navigator;o&&typeof o.sendBeacon==\"function\"?o.sendBeacon(e,new Blob([r],{type:\"application/json\"})):n.fetch(e,{method:\"POST\",headers:{\"Content-Type\":\"application/json\"},body:r,keepalive:!0})}catch(r){}}}function ln(e={}){var ge,be;let n=e.win||(typeof window!=\"undefined\"?window:null),t=e.doc||n&&n.document,r=e.now||(()=>Date.now()),o=e.endpoint||\"/api/doc\",s=(ge=e.idleMs)!=null?ge:Q,a=(be=e.sessionEveryMs)!=null?be:12e3,u=e.slug||\"\",d=e.internal||null,m=e.sessionId||an(),p=e.send||un(o,n),c={},l=0,g=0,f=0,w=0,h=null,E=r(),S=!1,B=!1,A=[],$e=()=>!!u||!!d,ce=i=>{if($e())try{p(i)}catch(b){}},V=()=>!!t&&t.visibilityState===\"visible\"&&!S,de=i=>{let b=r()-i,x=Math.max(0,E-i)+s;return Math.max(0,Math.min(b,x))},z=()=>{h!=null&&l>0&&(c[l]=(c[l]||0)+de(h)/1e3),h=V()?r():null},pe=()=>{z(),h=null},F=()=>{h==null&&V()&&(h=r())},j=()=>{let i=0;for(let b in c)i+=c[b];return h!=null&&(i+=de(h)/1e3),Math.round(i)},fe=()=>{let i={};for(let b in c)i[b]=Math.round(c[b]);return i},k=()=>{E=r(),S&&(S=!1,F())},me=(i,b)=>{d||ce({slug:u,event:i,page:b||g,maxPage:f,seconds:j(),sessionId:m})},I=()=>{z();let i={event:\"session\",sessionId:m,numPages:w,maxPage:f,totalSeconds:j(),pagesTime:fe()};d?(i.internal=!0,i.docId=d.docId,i.email=d.email,i.name=d.name,d.it&&(i.it=d.it)):i.slug=u,ce(i)},R=(i,b,x,ye)=>{!i||typeof i.addEventListener!=\"function\"||(i.addEventListener(b,x,ye),A.push(()=>{i.removeEventListener(b,x,ye)}))};return{sessionId:m,totalSeconds:j,maxPage:()=>f,pageTimes:fe,start(){if(B)return;B=!0,me(\"open\",1),l=1,E=r(),h=V()?r():null,R(t,\"visibilitychange\",()=>{t.visibilityState===\"hidden\"?(pe(),I()):(k(),F())}),R(n,\"focus\",()=>{k(),F()});for(let x of sn)R(n,x,k,{passive:!0});R(e.scrollElement,\"scroll\",k,{passive:!0}),R(n,\"beforeunload\",I);let i=n.setInterval(()=>{!S&&r()-E>s&&(S=!0,pe())},5e3),b=n.setInterval(I,a);A.push(()=>{n.clearInterval(i),n.clearInterval(b)})},setPage(i){!i||i===g||(E=r(),z(),l=i,g=i,i>f&&(f=i,me(\"page\",i)))},setPageCount(i){w=i||0},noteActivity:k,flush:I,stop(){for(;A.length;){let i=A.pop();try{i==null||i()}catch(b){}}B=!1}}}var U={};v(U,{STORAGE_KEYS:()=>M,attendeeKey:()=>fn,authorToken:()=>mn,avatarHtml:()=>ee,canModerate:()=>oe,createBudget:()=>Mn,createFileEcritures:()=>xn,createScheduler:()=>vn,escapeHtml:()=>y,fetchBorne:()=>wn,flattenPresence:()=>cn,formatMessageBody:()=>ne,initials:()=>Ce,isMentioned:()=>bn,isMine:()=>T,presenceId:()=>Tn,reactorId:()=>re,referenceAuteur:()=>gn,shouldNotify:()=>yn,unreadLabel:()=>hn});var M={attendeeKey:\"3dd-present-attkey\",authorToken:\"3dd-present-authtoken\",chatMuted:\"3dd-present-mute\",presenceId:\"3dd-present-uid\"};function y(e){return String(e==null?\"\":e).replace(/&/g,\"&\").replace(/</g,\"<\").replace(/>/g,\">\").replace(/\"/g,\""\")}function Ce(e){let n=(e||\"?\").trim().split(/\\s+/);return(((n[0]||\"\")[0]||\"?\")+((n[1]||\"\")[0]||\"\")).toUpperCase()}function ee(e,n){return e?`<img src=\"${y(e)}\" alt=\"\">`:y(Ce(n))}function ne(e){let n=y(e);return n=n.replace(/(https?:\\/\\/[^\\s<]+)/g,t=>`<a href=\"${t}\" target=_blank rel=noreferrer class=cm-link>${t}</a>`),n=n.replace(/(^|\\s)@([\\p{L}0-9_'.-]+)/gu,(t,r,o)=>`${r}<span class=cm-mention>@${o}</span>`),n}function cn(e){let n=new Map;for(let t in e||{})for(let r of(e||{})[t]||[]){let o=String(r.uid||r.email||r.name||\"\").toLowerCase()||`_${t}_${n.size}`;(!n.get(o)||r.role===\"presenter\")&&n.set(o,r)}return[...n.values()]}function dn(e,n){try{return e?e.getItem(n):null}catch(t){return null}}function pn(e,n,t){try{e==null||e.setItem(n,t)}catch(r){}}function fn(e,n=\"anon\",t=te){try{if(!e)return`anon-${n}`;let r=e.getItem(M.attendeeKey);if(r)return r;let o=`anon-${t()}`;return e.setItem(M.attendeeKey,o),o}catch(r){return`anon-${n}`}}function te(){let e=typeof globalThis!=\"undefined\"?globalThis.crypto:void 0;if(e&&typeof e.randomUUID==\"function\")return e.randomUUID().replace(/-/g,\"\");if(e&&typeof e.getRandomValues==\"function\"){let n=e.getRandomValues(new Uint8Array(24));return Array.from(n,t=>t.toString(16).padStart(2,\"0\")).join(\"\")}try{console.warn(\"[player] crypto indisponible : jeton d'auteur affaibli\")}catch(n){}return Math.random().toString(36).slice(2)+Math.random().toString(36).slice(2)}function mn(e,n=te){let t=dn(e,M.authorToken);if(t)return t;let r=n();return pn(e,M.authorToken,r),r}async function gn(e,n){let t=n!==void 0?n:typeof globalThis!=\"undefined\"&&globalThis.crypto?globalThis.crypto.subtle:null;if(!t||!e)return\"\";try{let r=async o=>{let s=await t.digest(\"SHA-256\",new TextEncoder().encode(o));return Array.from(new Uint8Array(s),a=>a.toString(16).padStart(2,\"0\")).join(\"\")};return(await r(\"ref:\"+await r(e))).slice(0,16)}catch(r){return\"\"}}function re(e){return String(e&&e.ref||\"\")}function T(e,n){return!e||!n||!e.author_ref||!n.ref?!1:e.author_ref===n.ref}function oe(e){return!!(e&&e.role===\"presenter\")}function bn(e,n){if(!n||!n.name)return!1;let t=(n.name.split(/\\s+/)[0]||\"\").toLowerCase();return t?String(e&&e.body||\"\").toLowerCase().includes(`@${t}`)&&!T(e,n):!1}function yn(e){let{msg:n,me:t,historyLoaded:r,chatHidden:o}=e;return!r||!n||n.deleted||T(n,t)?!1:o}function hn(e){return!e||e<=0?\"\":e>9?\"9+\":String(e)}function vn(e,n={}){var g;let t=(g=n.minMs)!=null?g:300,r=n.now||(()=>Date.now()),o=n.setTimer||((f,w)=>setTimeout(f,w)),s=n.clearTimer||(f=>clearTimeout(f)),a=!1,u=!1,d=-1/0,m=null,p=!1;function c(){if(p||a){u=!0;return}let f=t-(r()-d);if(f>0){l(f);return}a=!0,u=!1,d=r();let w=!1;e(()=>{w||(w=!0,a=!1,u&&!p&&c())})}function l(f){p||m!==null||(m=o(()=>{m=null,c()},Math.max(0,f)))}return{signaler(){p||c()},maintenant(){p||(d=-1/0,c())},arreter(){p=!0,m!==null&&(s(m),m=null)}}}function wn(e,n,t=J,r={}){let o=r.fetch||(typeof fetch==\"function\"?fetch:void 0);if(!o)return Promise.reject(new Error(\"fetch indisponible\"));let s=r.setTimer||((l,g)=>setTimeout(l,g)),a=r.clearTimer||(l=>clearTimeout(l)),u=null;try{u=new AbortController}catch(l){}let d=u?Me(G({},n||{}),{signal:u.signal}):G({},n||{}),m=null,p=new Promise((l,g)=>{m=s(()=>{try{u==null||u.abort()}catch(f){}g(new Error(\"delai\"))},t)}),c=()=>{try{a(m)}catch(l){}};return Promise.race([o(e,d),p]).then(l=>(c(),l),l=>{throw c(),l})}function Mn(e){let n=e.now||(()=>Date.now()),t=Math.max(1,e.rafale),r=t,o=n();return{prendre(){let s=n();return r=Math.min(t,r+(s-o)*e.parHeure/36e5),o=s,r<1?!1:(r-=1,!0)},restants:()=>r}}function xn(e={}){var m;let n=(m=e.minMs)!=null?m:0,t=e.now||(()=>Date.now()),r=e.setTimer||((p,c)=>setTimeout(p,c)),o=new Map,s=!1,a=-1/0,u=!1;function d(){if(s||o.size===0)return;let p=n-(t()-a);if(p>0){if(u)return;u=!0,r(()=>{u=!1,d()},p);return}let c=o.keys().next().value,l=o.get(c);o.delete(c),s=!0,a=t(),Promise.resolve().then(l.tache).then(g=>l.regler(g),()=>l.regler(null)).then(()=>{s=!1,d()})}return{poser(p,c){return new Promise(l=>{let g=o.get(p);o.set(p,{tache:c,regler:f=>{g&&g.regler(f),l(f)}}),d()})},enAttente:()=>o.size}}function Tn(e,n=te){try{if(e){let t=e.getItem(M.presenceId);if(t)return t;let r=`p-${n()}`;return e.setItem(M.presenceId,r),r}}catch(t){}return`p-${n()}`}var $={};v($,{messageClassName:()=>En,renderActions:()=>Oe,renderAttachment:()=>Ne,renderMessage:()=>_n,renderReactions:()=>Le});function Le(e,n){let t=e&&e.reactions||{},r=re(n),o=\"\";for(let s in t){let a=t[s]||[];if(!a.length)continue;let u=a.indexOf(r)>=0;o+=`<button class=\"re-chip${u?\" on\":\"\"}\" data-e=\"${y(s)}\">${y(s)} ${a.length}</button>`}return o}function Ne(e){if(!e||!e.url)return\"\";let n=y(e.url);if(e.kind===\"image\")return`<a class=cm-att href=\"${n}\" target=_blank rel=noreferrer><img src=\"${n}\" alt=\"\" loading=lazy></a>`;let t=y(e.name||\"Document.pdf\");return`<a class=cm-att-pdf href=\"${n}\" target=_blank rel=noreferrer data-pdf=\"${n}\"><span class=cm-att-ph></span><span class=cm-pdflabel>${t}</span></a>`}function Oe(e,n){let t=T(e,n.me),r=`<button class=cm-react title=\"R\\xE9agir\">${n.reactIcon||\"\"}</button><button class=cm-reply title=\"R\\xE9pondre\">\\u21A9</button>`;return t&&(r+='<button class=cm-edit title=\"Modifier\">\\u270E</button>'),(t||oe(n.me))&&(r+='<button class=cm-del-btn title=\"Supprimer\">\\u{1F5D1}</button>'),r}function _n(e,n){if(e.deleted)return'<span class=a></span><span class=b><div class=\"txt cm-del\">Message supprim\\xE9</div></span>';let t=e.reply_to?`<div class=cm-q><b>${y(e.reply_name||\"\")}</b> ${y(e.reply_text||\"\")}</div>`:\"\",r=e.edited?\" <span class=cm-ed>(modifi\\xE9)</span>\":\"\",o=e.body?`<div class=txt>${ne(e.body)}${r}</div>`:\"\",s=e.is_presenter?\" <span class=tag>pr\\xE9sentateur</span>\":\"\";return`<span class=a>${ee(e.author_avatar,e.author_name)}</span><span class=b><div class=who><b>${y(e.author_name||\"Invit\\xE9\")}</b>${s}</div>`+t+o+Ne(e.attachment)+`<div class=cm-re>${Le(e,n.me)}</div></span><span class=cm-act>${Oe(e,n)}</span>`}function En(e,n,t){return\"cm\"+(T(e,n)?\" me\":\"\")+(e.deleted?\" isdel\":\"\")+(t?\" mentioned\":\"\")}var H={};v(H,{DEFAULT_CENTER:()=>ae,DEFAULT_ZOOM:()=>ue,MAP_TYPES:()=>P,cycleMapType:()=>Sn,initialMapContent:()=>Rn,mapTypeLabel:()=>kn,sanitizeContent:()=>le});var P=[\"roadmap\",\"satellite\",\"hybrid\"],ae=[46.6,2.5],ue=6;function _(e){let n=Number(e);return Number.isFinite(n)?n:null}function se(e){if(!Array.isArray(e))return null;let n=_(e[0]),t=_(e[1]);return n!=null&&t!=null?[n,t]:null}var ie=(e,n,t)=>Math.max(n,Math.min(t,e));function le(e){if(!e||typeof e!=\"object\")return null;let n=e;if(n.kind===\"pdf\"||n.kind==null)return null;if(n.kind===\"streetview\"){let t=se(n.position);if(!t)return null;let r=n.pov&&typeof n.pov==\"object\"?n.pov:null,o=r?{heading:_(r.heading)||0,pitch:ie(_(r.pitch)||0,-90,90)}:{heading:0,pitch:0};return{kind:\"streetview\",position:t,pov:o,zoom:ie(_(n.zoom)||1,0,5)}}return n.kind!==\"map\"?null:{kind:\"map\",center:se(n.center)||[...ae],zoom:ie(Math.trunc(_(n.zoom)||ue),1,21),marker:se(n.marker),mapType:P.includes(n.mapType)?n.mapType:null,label:String(n.label||\"\").slice(0,160)||null}}function Sn(e){let n=P.indexOf(e);return P[(n+1)%P.length]}function kn(e){return e===\"roadmap\"?\"\\u{1F6F0} Satellite\":e===\"satellite\"?\"\\u{1F5FA} Hybride\":\"\\u{1F5FA} Plan\"}function Rn(){return{kind:\"map\",center:[...ae],zoom:ue,marker:null,mapType:null,label:null}}var D={};v(D,{CURRENT_PAGE_MARGIN:()=>Nn,DEFAULT_ASPECT:()=>In,MAX_ZOOM:()=>An,MIN_PAGE_WIDTH:()=>Cn,MIN_ZOOM:()=>Pn,PRERENDER_MARGIN:()=>Ln,arrowState:()=>$n,averageColor:()=>Wn,clampPage:()=>Ue,clampZoom:()=>On,fitWidth:()=>Dn,isImageDocument:()=>Bn,progressPercent:()=>Un,workerBlobUrl:()=>Vn});var Pn=.5,An=3,In=1.35,Cn=280,Ln=\"500px 0px\",Nn=\"-48% 0px -48% 0px\";function On(e){let n=Math.round((Number(e)||0)*10)/10;return Math.max(.5,Math.min(3,n))}function Ue(e,n){return Math.max(1,Math.min(n||1,Number(e)||1))}function Un(e,n){return n?Math.round(Ue(e,n)/n*100):0}function $n(e,n){return{prevDisabled:e<=1,nextDisabled:!!n&&e>=n}}var Hn=28;function Dn(e){var a;let n=(a=e.zoom)!=null?a:1,t=Math.max(280,e.containerWidth||900)*n;if(!e.onePage)return t;let r=e.aspect||1.35,s=((e.containerHeight||600)-Hn-(e.overlap||0)-(e.reserve||0))/r;return s>120?Math.min(t,s):t}function Wn(e){let n=e.length/4;if(!n)return\"rgb(0,0,0)\";let t=0,r=0,o=0;for(let s=0;s<e.length;s+=4)t+=e[s],r+=e[s+1],o+=e[s+2];return`rgb(${Math.round(t/n)},${Math.round(r/n)},${Math.round(o/n)})`}function Bn(e,n){return/[.](png|jpe?g|webp|gif|avif)($|[?])/i.test(String(e||n||\"\"))}async function Vn(e,n,t={}){let r=t.fetch||(typeof fetch==\"function\"?(a=>fetch(a)):null),o=t.subtle!==void 0?t.subtle:typeof globalThis!=\"undefined\"&&globalThis.crypto?globalThis.crypto.subtle:null,s=t.creerUrl||(a=>URL.createObjectURL(a));if(!r||!o||!n)return null;try{let a=await r(e);if(!a.ok)return null;let u=await a.arrayBuffer(),d=await o.digest(\"SHA-384\",u);return`sha384-${zn(new Uint8Array(d))}`!==n?null:s(new Blob([u],{type:\"application/javascript\"}))}catch(a){return null}}function zn(e){let n=\"\";for(let t of e)n+=String.fromCharCode(t);return btoa(n)}var W={};v(W,{presentationTransition:()=>Fn,switchDocUrl:()=>jn});function Fn(e,n={}){if(!e)return[{kind:\"nothing\"}];if(e.active===!1)return[{kind:\"ended\"}];let t=le(e.content);if(t)return[{kind:\"show-map\",content:t}];let r=[{kind:\"leave-map\"}];return e.file_url&&e.file_url!==n.docUrl?(r.push({kind:\"switch-doc\",url:e.file_url,name:e.file_name||void 0,title:e.doc_title||void 0,updatedAt:e.updated_at||void 0}),r):(e.current_page&&r.push({kind:\"show-page\",page:e.current_page}),r)}function jn(e,n){let t=encodeURIComponent(n||String(Date.now()));return`${e}${e.includes(\"?\")?\"&\":\"?\"}v=${t}`}return Fe(Gn);})();", SOURCE_HASH: "5bacfcb0f5b69825" };
|
|
7
|
+
module.exports = { PLAYER_BROWSER_JS: "\"use strict\";var Player=(()=>{var C=Object.defineProperty,He=Object.defineProperties,De=Object.getOwnPropertyDescriptor,We=Object.getOwnPropertyDescriptors,Be=Object.getOwnPropertyNames,he=Object.getOwnPropertySymbols;var we=Object.prototype.hasOwnProperty,Ve=Object.prototype.propertyIsEnumerable;var ve=(e,n,t)=>n in e?C(e,n,{enumerable:!0,configurable:!0,writable:!0,value:t}):e[n]=t,G=(e,n)=>{for(var t in n||(n={}))we.call(n,t)&&ve(e,t,n[t]);if(he)for(var t of he(n))Ve.call(n,t)&&ve(e,t,n[t]);return e},Me=(e,n)=>He(e,We(n));var v=(e,n)=>{for(var t in n)C(e,t,{get:n[t],enumerable:!0})},ze=(e,n,t,r)=>{if(n&&typeof n==\"object\"||typeof n==\"function\")for(let o of Be(n))!we.call(e,o)&&o!==t&&C(e,o,{get:()=>n[o],enumerable:!(r=De(n,o))||r.enumerable});return e};var Fe=e=>ze(C({},\"__esModule\",{value:!0}),e);var Gn={};v(Gn,{bridge:()=>L,cadence:()=>N,chat:()=>$,live:()=>U,presentation:()=>H,presentationState:()=>W,tracking:()=>O,viewer:()=>D});var L={};v(L,{onHostMessage:()=>en,onPlayerMessage:()=>Je,parseHostMessage:()=>_e,parsePlayerMessage:()=>Te,sendToHost:()=>qe,sendToPlayer:()=>Qe,toWire:()=>Y});var K=\"3dd-doc-\",je=/^[A-Za-z0-9_-]{1,64}$/,Ge=[\"close\",\"share\",\"embed-ready\",\"present-left\",\"present-denied\"],Ke=[\"embed-denied\"],Ye=[\"present-invite\",\"present-handover\",\"present-switch\"],Ze=[\"handover-done\"];function Y(e){let n={type:K+e.type};return\"slug\"in e&&e.slug&&(n.slug=e.slug),\"reason\"in e&&e.reason&&(n.reason=String(e.reason).slice(0,40)),n}function xe(e,n,t){if(!e||typeof e!=\"object\")return null;let r=e.type;if(typeof r!=\"string\"||!r.startsWith(K))return null;let o=r.slice(K.length);if(n.includes(o))return{type:o};if(Ke.includes(o)){let a=e.reason;return{type:o,reason:/^[a-z-]{1,40}$/.test(String(a||\"\"))?String(a):\"unknown\"}}if(!t.includes(o))return null;let s=e.slug;return typeof s!=\"string\"||!je.test(s)?null:{type:o,slug:s}}function Te(e){return xe(e,Ge,Ye)}function _e(e){return xe(e,Ze,[])}function qe(e,n){try{let t=n||(typeof window!=\"undefined\"?window.parent:null);t&&t.postMessage(Y(e),\"*\")}catch(t){}}function Qe(e,n){var t;try{(t=e==null?void 0:e.contentWindow)==null||t.postMessage(Y(n),\"*\")}catch(r){}}function Xe(e,n){var r;if(!e)return!0;let t=(r=e.contentWindow)!=null?r:e;return n===t}function Je(e,n,t){let r=n||(typeof window!=\"undefined\"?window:null);if(!r)return()=>{};let o=s=>{if(!Xe(t,s.source))return;let a=Te(s.data);a&&e(a)};return r.addEventListener(\"message\",o),()=>r.removeEventListener(\"message\",o)}function en(e,n){var s;let t=n||(typeof window!=\"undefined\"?window:null);if(!t)return()=>{};let r=(s=t.parent)!=null?s:null,o=a=>{if(!r||r===t||a.source!==r)return;let u=_e(a.data);u&&e(u)};return t.addEventListener(\"message\",o),()=>t.removeEventListener(\"message\",o)}var O={};v(O,{createTracker:()=>ln});var N={};v(N,{PRESENTER_ACTIONS_PER_HOUR:()=>X,PRESENT_CACHE_MS:()=>rn,PRESENT_QUOTA_MARGIN:()=>Ie,PRESENT_QUOTA_PER_HOUR:()=>on,PRESENT_READS_PER_HOUR:()=>Re,PRESENT_READ_BURST:()=>Pe,PRESENT_READ_COALESCE_MS:()=>Ae,PRESENT_RESYNC_MS:()=>Se,PRESENT_SIGNAL_BUDGET_PER_HOUR:()=>tn,PRESENT_WRITE_TIMEOUT_MS:()=>J,READERS_PER_EGRESS:()=>q,RESYNC_READS_PER_HOUR:()=>ke,SESSION_IDLE_MS:()=>Q,SESSION_INTERVAL_MS:()=>Z,SESSION_QUOTA_PER_HOUR:()=>nn,SESSION_WRITES_PER_HOUR:()=>Ee});var Z=12e3,Ee=Math.ceil(300),q=25,nn=Ee*q,Q=18e4,Se=25e3,ke=Math.ceil(36e5/Se),X=720,Re=ke+X,tn=X,Pe=20,J=1e4,Ae=400,rn=Ae,Ie=4,on=(Re+Pe)*q*Ie;var sn=[\"mousemove\",\"mousedown\",\"keydown\",\"wheel\",\"touchstart\",\"pointerdown\"];function an(){let e=typeof globalThis!=\"undefined\"?globalThis.crypto:void 0;if(e&&typeof e.randomUUID==\"function\")return e.randomUUID().replace(/-/g,\"\");if(e&&typeof e.getRandomValues==\"function\"){let n=e.getRandomValues(new Uint8Array(16));return Array.from(n,t=>t.toString(16).padStart(2,\"0\")).join(\"\")}return Math.random().toString(36).slice(2)+Date.now().toString(36)}function un(e,n){return t=>{try{let r=JSON.stringify(t),o=n.navigator;o&&typeof o.sendBeacon==\"function\"?o.sendBeacon(e,new Blob([r],{type:\"application/json\"})):n.fetch(e,{method:\"POST\",headers:{\"Content-Type\":\"application/json\"},body:r,keepalive:!0})}catch(r){}}}function ln(e={}){var ge,be;let n=e.win||(typeof window!=\"undefined\"?window:null),t=e.doc||n&&n.document,r=e.now||(()=>Date.now()),o=e.endpoint||\"/api/doc\",s=(ge=e.idleMs)!=null?ge:Q,a=(be=e.sessionEveryMs)!=null?be:12e3,u=e.slug||\"\",d=e.internal||null,m=e.sessionId||an(),p=e.send||un(o,n),c={},l=0,g=0,f=0,w=0,h=null,E=r(),S=!1,B=!1,A=[],$e=()=>!!u||!!d,ce=i=>{if($e())try{p(i)}catch(b){}},V=()=>!!t&&t.visibilityState===\"visible\"&&!S,de=i=>{let b=r()-i,x=Math.max(0,E-i)+s;return Math.max(0,Math.min(b,x))},z=()=>{h!=null&&l>0&&(c[l]=(c[l]||0)+de(h)/1e3),h=V()?r():null},pe=()=>{z(),h=null},F=()=>{h==null&&V()&&(h=r())},j=()=>{let i=0;for(let b in c)i+=c[b];return h!=null&&(i+=de(h)/1e3),Math.round(i)},fe=()=>{let i={};for(let b in c)i[b]=Math.round(c[b]);return i},k=()=>{E=r(),S&&(S=!1,F())},me=(i,b)=>{d||ce({slug:u,event:i,page:b||g,maxPage:f,seconds:j(),sessionId:m})},I=()=>{z();let i={event:\"session\",sessionId:m,numPages:w,maxPage:f,totalSeconds:j(),pagesTime:fe()};d?(i.internal=!0,i.docId=d.docId,i.email=d.email,i.name=d.name,d.it&&(i.it=d.it)):i.slug=u,ce(i)},R=(i,b,x,ye)=>{!i||typeof i.addEventListener!=\"function\"||(i.addEventListener(b,x,ye),A.push(()=>{i.removeEventListener(b,x,ye)}))};return{sessionId:m,totalSeconds:j,maxPage:()=>f,pageTimes:fe,start(){if(B)return;B=!0,me(\"open\",1),l=1,E=r(),h=V()?r():null,R(t,\"visibilitychange\",()=>{t.visibilityState===\"hidden\"?(pe(),I()):(k(),F())}),R(n,\"focus\",()=>{k(),F()});for(let x of sn)R(n,x,k,{passive:!0});R(e.scrollElement,\"scroll\",k,{passive:!0}),R(n,\"beforeunload\",I);let i=n.setInterval(()=>{!S&&r()-E>s&&(S=!0,pe())},5e3),b=n.setInterval(I,a);A.push(()=>{n.clearInterval(i),n.clearInterval(b)})},setPage(i){!i||i===g||(E=r(),z(),l=i,g=i,i>f&&(f=i,me(\"page\",i)))},setPageCount(i){w=i||0},noteActivity:k,flush:I,stop(){for(;A.length;){let i=A.pop();try{i==null||i()}catch(b){}}B=!1}}}var U={};v(U,{STORAGE_KEYS:()=>M,attendeeKey:()=>fn,authorToken:()=>mn,avatarHtml:()=>ee,canModerate:()=>oe,createBudget:()=>Mn,createFileEcritures:()=>xn,createScheduler:()=>vn,escapeHtml:()=>y,fetchBorne:()=>wn,flattenPresence:()=>cn,formatMessageBody:()=>ne,initials:()=>Ce,isMentioned:()=>bn,isMine:()=>T,presenceId:()=>Tn,reactorId:()=>re,referenceAuteur:()=>gn,shouldNotify:()=>yn,unreadLabel:()=>hn});var M={attendeeKey:\"3dd-present-attkey\",authorToken:\"3dd-present-authtoken\",chatMuted:\"3dd-present-mute\",presenceId:\"3dd-present-uid\"};function y(e){return String(e==null?\"\":e).replace(/&/g,\"&\").replace(/</g,\"<\").replace(/>/g,\">\").replace(/\"/g,\""\")}function Ce(e){let n=(e||\"?\").trim().split(/\\s+/);return(((n[0]||\"\")[0]||\"?\")+((n[1]||\"\")[0]||\"\")).toUpperCase()}function ee(e,n){return e?`<img src=\"${y(e)}\" alt=\"\">`:y(Ce(n))}function ne(e){let n=y(e);return n=n.replace(/(https?:\\/\\/[^\\s<]+)/g,t=>`<a href=\"${t}\" target=_blank rel=noreferrer class=cm-link>${t}</a>`),n=n.replace(/(^|\\s)@([\\p{L}0-9_'.-]+)/gu,(t,r,o)=>`${r}<span class=cm-mention>@${o}</span>`),n}function cn(e){let n=new Map;for(let t in e||{})for(let r of(e||{})[t]||[]){let o=String(r.uid||r.email||r.name||\"\").toLowerCase()||`_${t}_${n.size}`;(!n.get(o)||r.role===\"presenter\")&&n.set(o,r)}return[...n.values()]}function dn(e,n){try{return e?e.getItem(n):null}catch(t){return null}}function pn(e,n,t){try{e==null||e.setItem(n,t)}catch(r){}}function fn(e,n=\"anon\",t=te){try{if(!e)return`anon-${n}`;let r=e.getItem(M.attendeeKey);if(r)return r;let o=`anon-${t()}`;return e.setItem(M.attendeeKey,o),o}catch(r){return`anon-${n}`}}function te(){let e=typeof globalThis!=\"undefined\"?globalThis.crypto:void 0;if(e&&typeof e.randomUUID==\"function\")return e.randomUUID().replace(/-/g,\"\");if(e&&typeof e.getRandomValues==\"function\"){let n=e.getRandomValues(new Uint8Array(24));return Array.from(n,t=>t.toString(16).padStart(2,\"0\")).join(\"\")}try{console.warn(\"[player] crypto indisponible : jeton d'auteur affaibli\")}catch(n){}return Math.random().toString(36).slice(2)+Math.random().toString(36).slice(2)}function mn(e,n=te){let t=dn(e,M.authorToken);if(t)return t;let r=n();return pn(e,M.authorToken,r),r}async function gn(e,n){let t=n!==void 0?n:typeof globalThis!=\"undefined\"&&globalThis.crypto?globalThis.crypto.subtle:null;if(!t||!e)return\"\";try{let r=async o=>{let s=await t.digest(\"SHA-256\",new TextEncoder().encode(o));return Array.from(new Uint8Array(s),a=>a.toString(16).padStart(2,\"0\")).join(\"\")};return(await r(\"ref:\"+await r(e))).slice(0,16)}catch(r){return\"\"}}function re(e){return String(e&&e.ref||\"\")}function T(e,n){return!e||!n||!e.author_ref||!n.ref?!1:e.author_ref===n.ref}function oe(e){return!!(e&&e.role===\"presenter\")}function bn(e,n){if(!n||!n.name)return!1;let t=(n.name.split(/\\s+/)[0]||\"\").toLowerCase();return t?String(e&&e.body||\"\").toLowerCase().includes(`@${t}`)&&!T(e,n):!1}function yn(e){let{msg:n,me:t,historyLoaded:r,chatHidden:o}=e;return!r||!n||n.deleted||T(n,t)?!1:o}function hn(e){return!e||e<=0?\"\":e>9?\"9+\":String(e)}function vn(e,n={}){var g;let t=(g=n.minMs)!=null?g:300,r=n.now||(()=>Date.now()),o=n.setTimer||((f,w)=>setTimeout(f,w)),s=n.clearTimer||(f=>clearTimeout(f)),a=!1,u=!1,d=-1/0,m=null,p=!1;function c(){if(p||a){u=!0;return}let f=t-(r()-d);if(f>0){l(f);return}a=!0,u=!1,d=r();let w=!1;e(()=>{w||(w=!0,a=!1,u&&!p&&c())})}function l(f){p||m!==null||(m=o(()=>{m=null,c()},Math.max(0,f)))}return{signaler(){p||c()},maintenant(){p||(d=-1/0,c())},arreter(){p=!0,m!==null&&(s(m),m=null)}}}function wn(e,n,t=J,r={}){let o=r.fetch||(typeof fetch==\"function\"?fetch:void 0);if(!o)return Promise.reject(new Error(\"fetch indisponible\"));let s=r.setTimer||((l,g)=>setTimeout(l,g)),a=r.clearTimer||(l=>clearTimeout(l)),u=null;try{u=new AbortController}catch(l){}let d=u?Me(G({},n||{}),{signal:u.signal}):G({},n||{}),m=null,p=new Promise((l,g)=>{m=s(()=>{try{u==null||u.abort()}catch(f){}g(new Error(\"delai\"))},t)}),c=()=>{try{a(m)}catch(l){}};return Promise.race([o(e,d),p]).then(l=>(c(),l),l=>{throw c(),l})}function Mn(e){let n=e.now||(()=>Date.now()),t=Math.max(1,e.rafale),r=t,o=n();return{prendre(){let s=n();return r=Math.min(t,r+(s-o)*e.parHeure/36e5),o=s,r<1?!1:(r-=1,!0)},restants:()=>r}}function xn(e={}){var m;let n=(m=e.minMs)!=null?m:0,t=e.now||(()=>Date.now()),r=e.setTimer||((p,c)=>setTimeout(p,c)),o=new Map,s=!1,a=-1/0,u=!1;function d(){if(s||o.size===0)return;let p=n-(t()-a);if(p>0){if(u)return;u=!0,r(()=>{u=!1,d()},p);return}let c=o.keys().next().value,l=o.get(c);o.delete(c),s=!0,a=t(),Promise.resolve().then(l.tache).then(g=>l.regler(g),()=>l.regler(null)).then(()=>{s=!1,d()})}return{poser(p,c){return new Promise(l=>{let g=o.get(p);o.set(p,{tache:c,regler:f=>{g&&g.regler(f),l(f)}}),d()})},enAttente:()=>o.size}}function Tn(e,n=te){try{if(e){let t=e.getItem(M.presenceId);if(t)return t;let r=`p-${n()}`;return e.setItem(M.presenceId,r),r}}catch(t){}return`p-${n()}`}var $={};v($,{messageClassName:()=>En,renderActions:()=>Oe,renderAttachment:()=>Ne,renderMessage:()=>_n,renderReactions:()=>Le});function Le(e,n){let t=e&&e.reactions||{},r=re(n),o=\"\";for(let s in t){let a=t[s]||[];if(!a.length)continue;let u=a.indexOf(r)>=0;o+=`<button class=\"re-chip${u?\" on\":\"\"}\" data-e=\"${y(s)}\">${y(s)} ${a.length}</button>`}return o}function Ne(e){if(!e||!e.url)return\"\";let n=y(e.url);if(e.kind===\"image\")return`<a class=cm-att href=\"${n}\" target=_blank rel=noreferrer><img src=\"${n}\" alt=\"\" loading=lazy></a>`;let t=y(e.name||\"Document.pdf\");return`<a class=cm-att-pdf href=\"${n}\" target=_blank rel=noreferrer data-pdf=\"${n}\"><span class=cm-att-ph></span><span class=cm-pdflabel>${t}</span></a>`}function Oe(e,n){let t=T(e,n.me),r=`<button class=cm-react title=\"R\\xE9agir\">${n.reactIcon||\"\"}</button><button class=cm-reply title=\"R\\xE9pondre\">\\u21A9</button>`;return t&&(r+='<button class=cm-edit title=\"Modifier\">\\u270E</button>'),(t||oe(n.me))&&(r+='<button class=cm-del-btn title=\"Supprimer\">\\u{1F5D1}</button>'),r}function _n(e,n){if(e.deleted)return'<span class=a></span><span class=b><div class=\"txt cm-del\">Message supprim\\xE9</div></span>';let t=e.reply_to?`<div class=cm-q><b>${y(e.reply_name||\"\")}</b> ${y(e.reply_text||\"\")}</div>`:\"\",r=e.edited?\" <span class=cm-ed>(modifi\\xE9)</span>\":\"\",o=e.body?`<div class=txt>${ne(e.body)}${r}</div>`:\"\",s=e.is_presenter?\" <span class=tag>pr\\xE9sentateur</span>\":\"\";return`<span class=a>${ee(e.author_avatar,e.author_name)}</span><span class=b><div class=who><b>${y(e.author_name||\"Invit\\xE9\")}</b>${s}</div>`+t+o+Ne(e.attachment)+`<div class=cm-re>${Le(e,n.me)}</div></span><span class=cm-act>${Oe(e,n)}</span>`}function En(e,n,t){return\"cm\"+(T(e,n)?\" me\":\"\")+(e.deleted?\" isdel\":\"\")+(t?\" mentioned\":\"\")}var H={};v(H,{DEFAULT_CENTER:()=>ae,DEFAULT_ZOOM:()=>ue,MAP_TYPES:()=>P,cycleMapType:()=>Sn,initialMapContent:()=>Rn,mapTypeLabel:()=>kn,sanitizeContent:()=>le});var P=[\"roadmap\",\"satellite\",\"hybrid\"],ae=[46.6,2.5],ue=6;function _(e){let n=Number(e);return Number.isFinite(n)?n:null}function se(e){if(!Array.isArray(e))return null;let n=_(e[0]),t=_(e[1]);return n!=null&&t!=null?[n,t]:null}var ie=(e,n,t)=>Math.max(n,Math.min(t,e));function le(e){if(!e||typeof e!=\"object\")return null;let n=e;if(n.kind===\"pdf\"||n.kind==null)return null;if(n.kind===\"streetview\"){let t=se(n.position);if(!t)return null;let r=n.pov&&typeof n.pov==\"object\"?n.pov:null,o=r?{heading:_(r.heading)||0,pitch:ie(_(r.pitch)||0,-90,90)}:{heading:0,pitch:0};return{kind:\"streetview\",position:t,pov:o,zoom:ie(_(n.zoom)||1,0,5)}}return n.kind!==\"map\"?null:{kind:\"map\",center:se(n.center)||[...ae],zoom:ie(Math.trunc(_(n.zoom)||ue),1,21),marker:se(n.marker),mapType:P.includes(n.mapType)?n.mapType:null,label:String(n.label||\"\").slice(0,160)||null}}function Sn(e){let n=P.indexOf(e);return P[(n+1)%P.length]}function kn(e){return e===\"roadmap\"?\"\\u{1F6F0} Satellite\":e===\"satellite\"?\"\\u{1F5FA} Hybride\":\"\\u{1F5FA} Plan\"}function Rn(){return{kind:\"map\",center:[...ae],zoom:ue,marker:null,mapType:null,label:null}}var D={};v(D,{CURRENT_PAGE_MARGIN:()=>Nn,DEFAULT_ASPECT:()=>In,MAX_ZOOM:()=>An,MIN_PAGE_WIDTH:()=>Cn,MIN_ZOOM:()=>Pn,PRERENDER_MARGIN:()=>Ln,arrowState:()=>$n,averageColor:()=>Wn,clampPage:()=>Ue,clampZoom:()=>On,fitWidth:()=>Dn,isImageDocument:()=>Bn,progressPercent:()=>Un,workerBlobUrl:()=>Vn});var Pn=.5,An=3,In=1.35,Cn=280,Ln=\"500px 0px\",Nn=\"-48% 0px -48% 0px\";function On(e){let n=Math.round((Number(e)||0)*10)/10;return Math.max(.5,Math.min(3,n))}function Ue(e,n){return Math.max(1,Math.min(n||1,Number(e)||1))}function Un(e,n){return n?Math.round(Ue(e,n)/n*100):0}function $n(e,n){return{prevDisabled:e<=1,nextDisabled:!!n&&e>=n}}var Hn=28;function Dn(e){var a;let n=(a=e.zoom)!=null?a:1,t=Math.max(280,e.containerWidth||900)*n;if(!e.onePage)return t;let r=e.aspect||1.35,s=((e.containerHeight||600)-Hn-(e.overlap||0)-(e.reserve||0))/r;return s>120?Math.min(t,s):t}function Wn(e){let n=e.length/4;if(!n)return\"rgb(0,0,0)\";let t=0,r=0,o=0;for(let s=0;s<e.length;s+=4)t+=e[s],r+=e[s+1],o+=e[s+2];return`rgb(${Math.round(t/n)},${Math.round(r/n)},${Math.round(o/n)})`}function Bn(e,n){return/[.](png|jpe?g|webp|gif|avif)($|[?])/i.test(String(e||n||\"\"))}async function Vn(e,n,t={}){let r=t.fetch||(typeof fetch==\"function\"?(a=>fetch(a)):null),o=t.subtle!==void 0?t.subtle:typeof globalThis!=\"undefined\"&&globalThis.crypto?globalThis.crypto.subtle:null,s=t.creerUrl||(a=>URL.createObjectURL(a));if(!r||!o||!n)return null;try{let a=await r(e);if(!a.ok)return null;let u=await a.arrayBuffer(),d=await o.digest(\"SHA-384\",u);return`sha384-${zn(new Uint8Array(d))}`!==n?null:s(new Blob([u],{type:\"application/javascript\"}))}catch(a){return null}}function zn(e){let n=\"\";for(let t of e)n+=String.fromCharCode(t);return btoa(n)}var W={};v(W,{presentationTransition:()=>Fn,switchDocUrl:()=>jn});function Fn(e,n={}){if(!e)return[{kind:\"nothing\"}];if(e.active===!1)return[{kind:\"ended\"}];let t=le(e.content);if(t)return[{kind:\"show-map\",content:t}];let r=[{kind:\"leave-map\"}];return e.file_url&&e.file_url!==n.docUrl?(r.push({kind:\"switch-doc\",url:e.file_url,name:e.file_name||void 0,title:e.doc_title||void 0,updatedAt:e.updated_at||void 0}),r):(e.current_page&&r.push({kind:\"show-page\",page:e.current_page}),r)}function jn(e,n){let t=encodeURIComponent(n||String(Date.now()));return`${e}${e.includes(\"?\")?\"&\":\"?\"}v=${t}`}return Fe(Gn);})();", SOURCE_HASH: "0e1b2ed50563dde4" };
|
package/server/handler.js
CHANGED
|
@@ -434,7 +434,7 @@ var Live=(function(){
|
|
|
434
434
|
// s'éteindre, recommence, et rebascule encore. Rejouer la même intention deux fois donne le
|
|
435
435
|
// même résultat qu'une fois ; c'est ce que le réseau exige.
|
|
436
436
|
var _m=msgData[id]||{},_rs=(_m.reactions&&_m.reactions[e])||[],veut=_rs.indexOf(MOIREF)<0;
|
|
437
|
-
fetch('/api/doc',{method:'POST',headers:{'Content-Type':'application/json'},body:JSON.stringify({action:'present-react',slug:SLUG,msgId:+id,emoji:e,
|
|
437
|
+
fetch('/api/doc',{method:'POST',headers:{'Content-Type':'application/json'},body:JSON.stringify({action:'present-react',slug:SLUG,msgId:+id,emoji:e,authorToken:authToken(),etat:veut})}).then(majDiffusee).catch(function(){});}
|
|
438
438
|
function setReply(id){var m=msgData[id];if(!m||m.deleted)return;var nm=m.author_name||'Invité';replyCtx={id:+id,name:nm,text:(m.body||'').slice(0,120)};var el=document.getElementById('chatReply');if(el){el.style.display='flex';el.innerHTML='<span class=cq><b>'+esc(nm)+'</b> '+esc((m.body||'').slice(0,80))+'</span><button id=chatReplyX title=Annuler>×</button>';var x=document.getElementById('chatReplyX');if(x)x.addEventListener('click',clearReply);}var t=document.getElementById('chatText');if(t)t.focus();}
|
|
439
439
|
function clearReply(){replyCtx=null;var el=document.getElementById('chatReply');if(el){el.style.display='none';el.innerHTML='';}}
|
|
440
440
|
function send(){var i=document.getElementById('chatText');var t=(i.value||'').trim();if(!t||!ME)return;if(LOCKED&&!canMod())return;i.value='';toggleSend();
|
|
@@ -1560,7 +1560,13 @@ const PLAFOND_RELAIS = Number(process.env.PLAYER_MAX_RELAY_BYTES || 0) || 60 * 1
|
|
|
1560
1560
|
|
|
1561
1561
|
async function relayerFichier(res, r, disposition) {
|
|
1562
1562
|
if (!r) { res.statusCode = 404; res.end("Fichier indisponible"); return; }
|
|
1563
|
-
|
|
1563
|
+
// 413 et 416 sont des REFUS ARGUMENTÉS de l'amont local (plafond, borne absurde) : les fondre
|
|
1564
|
+
// dans un 502 dirait « panne » là où l'amont a dit « demande irrecevable ».
|
|
1565
|
+
if (!r.ok && r.status !== 206) {
|
|
1566
|
+
res.statusCode = r.status === 413 || r.status === 416 ? r.status : 502;
|
|
1567
|
+
res.end(r.status === 413 ? "Fichier trop volumineux" : "Fichier indisponible");
|
|
1568
|
+
return;
|
|
1569
|
+
}
|
|
1564
1570
|
|
|
1565
1571
|
const compresse = !!r.headers.get("content-encoding");
|
|
1566
1572
|
if (compresse && r.status === 206) { res.statusCode = 502; res.end("Fichier indisponible"); return; }
|
|
@@ -3398,7 +3404,16 @@ async function handler(req, res) {
|
|
|
3398
3404
|
const ip = adresseAppelant(req) || "anon";
|
|
3399
3405
|
const allowed = await PLAYER.limits.allow(`preact:${ip}`, 200, 3600);
|
|
3400
3406
|
if (!allowed) return jp(429, { ok: false, error: "rate" });
|
|
3401
|
-
|
|
3407
|
+
// ⚠️ LE CINQUIÈME ARGUMENT A MANQUÉ ICI PENDANT TROIS VERSIONS. `toggleReaction` sait
|
|
3408
|
+
// poser un état depuis 0.1.56, le client l'envoie depuis 0.1.56 — et cette ligne le
|
|
3409
|
+
// jetait. Les essais éprouvaient la FONCTION, jamais la route : le correctif était vu
|
|
3410
|
+
// refuser en mutation et inactif en production. Trouvé par le troisième audit.
|
|
3411
|
+
// ⚠️ `body.reactor` N'EST PLUS LU. C'est un champ que le client CHOISIT, et les refs des
|
|
3412
|
+
// autres participants sont publics (le tableau des réactions les porte) : s'y fier
|
|
3413
|
+
// laissait n'importe qui poser ou retirer les réactions d'un autre. L'identité se
|
|
3414
|
+
// dérive du jeton d'auteur — le secret qui ne quitte pas le navigateur qui l'a tiré.
|
|
3415
|
+
const reacteur = require("./presentations").reacteurDepuisJeton(body.authorToken);
|
|
3416
|
+
const r = await toggleReaction(String(body.slug || ""), body.msgId, body.emoji, reacteur, body.etat);
|
|
3402
3417
|
return jp(r.ok ? 200 : (r.status || 400), r);
|
|
3403
3418
|
} catch { return jp(500, { ok: false }); }
|
|
3404
3419
|
}
|
|
@@ -3832,9 +3847,14 @@ async function handler(req, res) {
|
|
|
3832
3847
|
// carte est ce qu'un hôte interroge déjà pour épingler sa version ; c'est donc ici.
|
|
3833
3848
|
// Elle RAPPORTE ce qui est connu, elle ne sonde pas : un diagnostic ne doit pas tomber en
|
|
3834
3849
|
// même temps que ce qu'il diagnostique.
|
|
3835
|
-
|
|
3836
|
-
|
|
3837
|
-
|
|
3850
|
+
// ⚠️ `couvre` DIT LA PORTÉE, parce que « complet » sans portée surpromet : les migrations de
|
|
3851
|
+
// limites (0003/0004) n'y sont PAS — un hôte peut fournir sa propre capacité `limits`, et
|
|
3852
|
+
// chez lui leur absence est normale. Ce champ empêche de lire « complet » comme « tout le
|
|
3853
|
+
// dossier supabase/ est appliqué ». Relevé par le troisième audit.
|
|
3854
|
+
schema: { couvre: "colonnes-conditionnelles",
|
|
3855
|
+
...(String(q.schema || "") === "1"
|
|
3856
|
+
? await require("./schema").sonderTout()
|
|
3857
|
+
: require("./schema").etatDuSchema()) },
|
|
3838
3858
|
// « L'hôte peut-il créer un lien en son nom propre ? » — configuré, pas seulement
|
|
3839
3859
|
// possible. Un hôte qui oublie le secret reçoit un 401 qui ressemble à un droit
|
|
3840
3860
|
// manquant ; ce booléen le lui dit sans qu'il ait à essayer.
|
package/server/presentations.js
CHANGED
|
@@ -55,7 +55,15 @@ async function reclaimPresentation(slug, email) {
|
|
|
55
55
|
// docs/MIGRATIONS.md décrit : PostgREST rejette le PATCH ENTIER si la colonne manque. La reprise
|
|
56
56
|
// aurait cessé de fonctionner chez tout hôte non migré — pas la nouvelle garantie, la reprise.
|
|
57
57
|
const rangDispo = await require("./schema").attendue("rangEcriture");
|
|
58
|
-
|
|
58
|
+
// ⚠️ LE PROPRIÉTAIRE DANS LA CONDITION, pas seulement dans la vérification. Entre la lecture et
|
|
59
|
+
// l'écriture, un transfert a pu donner la présentation à quelqu'un d'autre : la reprise de
|
|
60
|
+
// l'ANCIEN propriétaire arrivait quand même, régénérait le jeton, et volait la session au
|
|
61
|
+
// nouveau — précisément ce que le transfert venait d'accorder. Constat du troisième audit.
|
|
62
|
+
const ecrit = await ecrireSiEncoreVrai(
|
|
63
|
+
`doc_presentations?slug=eq.${enc(slug)}&owner_email=eq.${enc(row.owner_email)}`,
|
|
64
|
+
{ control_hash: sha(control), active: true, ...(rangDispo ? { write_seq: 0 } : {}), last_seen: new Date().toISOString(), updated_at: new Date().toISOString() },
|
|
65
|
+
);
|
|
66
|
+
if (!ecrit) return { ok: false, status: 409 };
|
|
59
67
|
return { ok: true, slug, control, page: row.current_page || 1, fileUrl: row.file_url, fileName: row.file_name, docTitle: row.doc_title, docId: row.doc_id };
|
|
60
68
|
}
|
|
61
69
|
|
|
@@ -65,8 +73,15 @@ async function touchPresentation(slug, control) {
|
|
|
65
73
|
const row = await getPresentation(slug);
|
|
66
74
|
if (!row) return { ok: false, status: 404 };
|
|
67
75
|
if (!tokenMatches(control, row.control_hash)) return { ok: false, status: 403 };
|
|
68
|
-
|
|
69
|
-
|
|
76
|
+
// ⚠️ LE JETON DANS LA CONDITION : une reprise ailleurs le régénère, une clôture l'annule. Sans
|
|
77
|
+
// ça, le heartbeat d'un ANCIEN onglet maintenait « vivante » une session qui ne lui appartient
|
|
78
|
+
// plus — et l'auto-purge, qui se fie à `last_seen`, ne fermait jamais l'orpheline.
|
|
79
|
+
const ecrit = await ecrireSiEncoreVrai(
|
|
80
|
+
`doc_presentations?slug=eq.${enc(slug)}&control_hash=eq.${sha(control)}`,
|
|
81
|
+
{ last_seen: new Date().toISOString() },
|
|
82
|
+
);
|
|
83
|
+
// Un heartbeat refusé dit à l'onglet périmé de s'arrêter — c'est une information, pas une panne.
|
|
84
|
+
return ecrit ? { ok: true } : { ok: false, status: 409 };
|
|
70
85
|
}
|
|
71
86
|
|
|
72
87
|
// Liste des présentations en cours (membre authentifié). Auto-purge : une présentation active dont le
|
|
@@ -108,7 +123,12 @@ async function listActivePresentations(email) {
|
|
|
108
123
|
const now = Date.now();
|
|
109
124
|
const stale = list.filter((r) => now - new Date(r.last_seen || r.updated_at || 0).getTime() > STALE_MS).map((r) => r.slug);
|
|
110
125
|
if (stale.length) {
|
|
111
|
-
|
|
126
|
+
// ⚠️ L'INACTIVITÉ DANS LA CONDITION, pas seulement dans le calcul. Entre la lecture de la liste
|
|
127
|
+
// et cette écriture, une présentation « orpheline » a pu envoyer son heartbeat : la purger
|
|
128
|
+
// quand même désactivait une session redevenue vivante, sous les pieds de son présentateur.
|
|
129
|
+
// `lte` sur le seuil : seule une ligne dont last_seen n'a PAS bougé est purgée.
|
|
130
|
+
const seuil = new Date(now - STALE_MS).toISOString();
|
|
131
|
+
await PLAYER.db.request(`doc_presentations?slug=in.(${stale.map((s) => enc(s)).join(",")})&active=eq.true&last_seen=lte.${enc(seuil)}`, { method: "PATCH", headers: { Prefer: "return=minimal" }, body: { active: false, updated_at: new Date().toISOString() } }).catch(() => {});
|
|
112
132
|
}
|
|
113
133
|
const live = list.filter((r) => !stale.includes(r.slug));
|
|
114
134
|
return live.map((r) => ({ slug: r.slug, docId: r.doc_id, fileUrl: r.file_url, fileName: r.file_name, docTitle: r.doc_title, presenterName: r.presenter_name, ownerName: r.owner_name, ownerAvatar: r.owner_avatar, currentPage: r.current_page || 1, mine: !!(me && r.owner_email && r.owner_email === me), updatedAt: r.updated_at }));
|
|
@@ -148,8 +168,18 @@ async function endPresentationByOwner(slug, email, isAdmin) {
|
|
|
148
168
|
const row = await getPresentation(slug);
|
|
149
169
|
if (!row) return { ok: false, status: 404 };
|
|
150
170
|
if (!isAdmin && (!row.owner_email || row.owner_email !== lc(email))) return { ok: false, status: 403 };
|
|
151
|
-
|
|
152
|
-
|
|
171
|
+
// ⚠️ LE PROPRIÉTAIRE DANS LA CONDITION (l'admin, lui, ferme ce qu'il veut : modération). Une
|
|
172
|
+
// clôture retardée émise par l'ANCIEN propriétaire après un transfert fermait la session du
|
|
173
|
+
// nouveau — le contraire de ce que le transfert venait d'accorder.
|
|
174
|
+
if (isAdmin) {
|
|
175
|
+
await PLAYER.db.request(`doc_presentations?slug=eq.${enc(slug)}`, { method: "PATCH", headers: { Prefer: "return=minimal" }, body: { ...CLOTURE, updated_at: new Date().toISOString() } });
|
|
176
|
+
return { ok: true };
|
|
177
|
+
}
|
|
178
|
+
const ecrit = await ecrireSiEncoreVrai(
|
|
179
|
+
`doc_presentations?slug=eq.${enc(slug)}&owner_email=eq.${enc(row.owner_email)}`,
|
|
180
|
+
{ ...CLOTURE, updated_at: new Date().toISOString() },
|
|
181
|
+
);
|
|
182
|
+
return ecrit ? { ok: true } : { ok: false, status: 409 };
|
|
153
183
|
}
|
|
154
184
|
|
|
155
185
|
// Transfert de contrôle : le propriétaire actuel (JWT) désigne un nouveau membre propriétaire (par email).
|
|
@@ -161,8 +191,14 @@ async function handoverPresentation(slug, currentEmail, newOwner) {
|
|
|
161
191
|
const row = await getPresentation(slug);
|
|
162
192
|
if (!row) return { ok: false, status: 404 };
|
|
163
193
|
if (!row.owner_email || row.owner_email !== lc(currentEmail)) return { ok: false, status: 403 };
|
|
164
|
-
|
|
165
|
-
|
|
194
|
+
// ⚠️ Deux transferts concurrents depuis le même propriétaire : le premier écrit gagne, le second
|
|
195
|
+
// ne trouve plus sa ligne — sans condition, il ÉCRASAIT le premier et le document changeait de
|
|
196
|
+
// mains deux fois, dans un ordre que personne n'avait choisi.
|
|
197
|
+
const ecrit = await ecrireSiEncoreVrai(
|
|
198
|
+
`doc_presentations?slug=eq.${enc(slug)}&owner_email=eq.${enc(lc(currentEmail))}`,
|
|
199
|
+
{ owner_user_id: o.id ? String(o.id) : null, owner_email: lc(o.email), owner_name: (o.name || "").slice(0, 120) || null, owner_avatar: (o.avatar || "").slice(0, 600) || null, updated_at: new Date().toISOString() },
|
|
200
|
+
);
|
|
201
|
+
return ecrit ? { ok: true, slug } : { ok: false, status: 409 };
|
|
166
202
|
}
|
|
167
203
|
|
|
168
204
|
async function getPresentation(slug) {
|
|
@@ -350,6 +386,21 @@ const CHAMPS_PUBLICS = [
|
|
|
350
386
|
*/
|
|
351
387
|
const refAuteur = (hash) => (hash ? sha("ref:" + hash).slice(0, 16) : null);
|
|
352
388
|
|
|
389
|
+
/**
|
|
390
|
+
* ⚠️ L'IDENTITÉ D'UN RÉACTEUR SE DÉRIVE D'UN SECRET, ELLE NE SE DÉCLARE PAS.
|
|
391
|
+
*
|
|
392
|
+
* Le client envoyait `reactor: MOIREF` — or MOIREF est PUBLIC : chaque participant reçoit les refs
|
|
393
|
+
* de tous les autres dans le tableau des réactions (il en a besoin pour dessiner « j'ai réagi »).
|
|
394
|
+
* N'importe qui pouvait donc copier le ref d'un autre et poser ou retirer SES réactions. Constat
|
|
395
|
+
* du troisième audit, vérifié : les refs transitent en clair dans `reactionsPubliques`.
|
|
396
|
+
*
|
|
397
|
+
* Le jeton d'auteur, lui, ne quitte jamais le navigateur qui l'a tiré. La dérivation est la même
|
|
398
|
+
* que côté client (`referenceAuteur` dans src/live.ts) : sha("ref:" + sha(jeton)), tronqué à 16 —
|
|
399
|
+
* si les deux divergent, « mes » réactions cessent de s'afficher comme miennes, et l'essai qui
|
|
400
|
+
* confronte les deux moteurs le dira.
|
|
401
|
+
*/
|
|
402
|
+
const reacteurDepuisJeton = (jeton) => (jeton ? refAuteur(sha(String(jeton))) : null);
|
|
403
|
+
|
|
353
404
|
/**
|
|
354
405
|
* ⚠️ LES ANCIENNES RÉACTIONS PORTENT DES ADRESSES, et aucune migration ne les réécrira : le
|
|
355
406
|
* client stockait `email || nom` comme identité de réacteur. On garde le COMPTE — la pastille
|
|
@@ -504,8 +555,13 @@ async function setChatLock(slug, control, locked) {
|
|
|
504
555
|
const pres = await getPresentation(slug);
|
|
505
556
|
if (!pres) return { ok: false, status: 404 };
|
|
506
557
|
if (!tokenMatches(control, pres.control_hash)) return { ok: false, status: 403 };
|
|
507
|
-
|
|
508
|
-
|
|
558
|
+
// Le jeton dans la condition : un verrou posé APRÈS une clôture ou une reprise ailleurs ne
|
|
559
|
+
// trouve plus sa ligne — il ne s'applique pas à une session qui n'est plus la sienne.
|
|
560
|
+
const ecrit = await ecrireSiEncoreVrai(
|
|
561
|
+
`doc_presentations?slug=eq.${enc(String(slug || ""))}&control_hash=eq.${sha(control)}`,
|
|
562
|
+
{ chat_locked: !!locked },
|
|
563
|
+
);
|
|
564
|
+
return ecrit ? { ok: true } : { ok: false, status: 409 };
|
|
509
565
|
}
|
|
510
566
|
|
|
511
567
|
// Réaction emoji (toggle) : le participant (identifié par email ou nom) ajoute/retire un emoji sur un message.
|
|
@@ -532,7 +588,10 @@ async function toggleReaction(slug, msgId, emoji, reactor, etat) {
|
|
|
532
588
|
// participants comme une réaction, ce qui est absurde même sans être dangereux. Deux barrières,
|
|
533
589
|
// parce que la seconde protège le jour où quelqu'un lève le plafond de longueur.
|
|
534
590
|
if (/[A-Za-z_$]/.test(e)) return { ok: false, status: 400 };
|
|
535
|
-
|
|
591
|
+
// ⚠️ LE RANG N'EST DEMANDÉ QUE LÀ OÙ IL EXISTE — sélectionner une colonne absente fait échouer
|
|
592
|
+
// la LECTURE entière chez un hôte non migré, et ce serait les réactions qu'on casserait.
|
|
593
|
+
const ordonne = await require("./schema").attendue("reactionsOrdonnees");
|
|
594
|
+
const rows = await PLAYER.db.request(`doc_presentation_messages?id=eq.${id}&slug=eq.${enc(String(slug || ""))}&select=${ordonne ? "reactions,reactions_seq" : "reactions"}&limit=1`);
|
|
536
595
|
// ⚠️ LE NOM DE PROPRIÉTÉ ÉCRIT ICI VIENT DU CLIENT — et la garde posée en 0.1.2 ne couvrait que
|
|
537
596
|
// les LECTURES. `Object.hasOwn` empêchait de LIRE `constructor` ; rien n'empêchait de l'ÉCRIRE.
|
|
538
597
|
//
|
|
@@ -547,16 +606,35 @@ async function toggleReaction(slug, msgId, emoji, reactor, etat) {
|
|
|
547
606
|
//
|
|
548
607
|
// Un objet SANS prototype retire la classe entière : il n'y a plus rien à masquer ni à
|
|
549
608
|
// atteindre, quelle que soit la clé et quel que soit le plafond.
|
|
550
|
-
|
|
551
|
-
|
|
552
|
-
|
|
553
|
-
|
|
554
|
-
|
|
555
|
-
|
|
556
|
-
|
|
557
|
-
|
|
558
|
-
|
|
559
|
-
|
|
609
|
+
// ⚠️ LIRE-MODIFIER-RÉÉCRIRE PERD DES ÉCRITURES : deux participants qui réagissent dans la même
|
|
610
|
+
// seconde lisent le même JSON, et la seconde réécriture emporte la première — une réaction
|
|
611
|
+
// disparaît, sans erreur nulle part. Même remède que le pilotage (0002) : l'écriture porte le
|
|
612
|
+
// rang qu'elle a LU, la base ne l'accepte que s'il n'a pas bougé, sinon on relit et on rejoue.
|
|
613
|
+
// Trois rejeux suffisent : chaque tour, au moins UN écrivain gagne — l'attente est bornée par le
|
|
614
|
+
// nombre de concurrents réels, pas par la malchance.
|
|
615
|
+
let lu = rows;
|
|
616
|
+
for (let essai = 0; essai < 4; essai += 1) {
|
|
617
|
+
const brut = (Array.isArray(lu) && lu[0] && lu[0].reactions && typeof lu[0].reactions === "object" && !Array.isArray(lu[0].reactions)) ? lu[0].reactions : {};
|
|
618
|
+
const rang = ordonne ? Math.trunc(Number((Array.isArray(lu) && lu[0] && lu[0].reactions_seq) || 0)) : 0;
|
|
619
|
+
// Objet NU déclaré tel quel : la garde statique des écritures indexées reconnaît la
|
|
620
|
+
// déclaration, pas un voisinage — et c'est elle qui a refusé la première forme de cette boucle.
|
|
621
|
+
const cur = Object.create(null);
|
|
622
|
+
Object.assign(cur, brut);
|
|
623
|
+
const arr = Array.isArray(cur[e]) ? cur[e] : [];
|
|
624
|
+
const i = arr.indexOf(who);
|
|
625
|
+
const veut = etat === undefined || etat === null ? (i < 0) : !!etat;
|
|
626
|
+
if (veut && i < 0) arr.push(who);
|
|
627
|
+
if (!veut && i >= 0) arr.splice(i, 1);
|
|
628
|
+
if (arr.length) cur[e] = arr; else delete cur[e];
|
|
629
|
+
const condition = ordonne ? `&reactions_seq=eq.${rang}` : "";
|
|
630
|
+
const maj = await PLAYER.db.request(`doc_presentation_messages?id=eq.${id}${condition}&select=*`, { method: "PATCH", headers: { Prefer: "return=representation" }, body: { reactions: cur, ...(ordonne ? { reactions_seq: rang + 1 } : {}) } });
|
|
631
|
+
if (!ordonne || (Array.isArray(maj) && maj.length)) return { ok: true, message: premierPublic(maj) };
|
|
632
|
+
// Rang dépassé : quelqu'un a écrit entre notre lecture et notre écriture. On relit, on rejoue.
|
|
633
|
+
lu = await PLAYER.db.request(`doc_presentation_messages?id=eq.${id}&slug=eq.${enc(String(slug || ""))}&select=reactions,reactions_seq&limit=1`);
|
|
634
|
+
}
|
|
635
|
+
// Quatre tours perdus : plus de concurrents que de raisons d'attendre. Un refus net vaut mieux
|
|
636
|
+
// qu'une écriture qui écraserait ce que les gagnants viennent de poser.
|
|
637
|
+
return { ok: false, status: 409 };
|
|
560
638
|
}
|
|
561
639
|
|
|
562
640
|
// ── Statistiques de présentation (assistance) ────────────────────────────────────────────────────────────
|
|
@@ -767,4 +845,5 @@ async function listPresentationsForDoc(docId, email, isAdmin, autoriseLarge) {
|
|
|
767
845
|
}
|
|
768
846
|
|
|
769
847
|
module.exports = {
|
|
848
|
+
reacteurDepuisJeton,
|
|
770
849
|
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 };
|
package/server/schema.js
CHANGED
|
@@ -42,6 +42,11 @@ const ATTENDUES = {
|
|
|
42
42
|
migration: "supabase/migrations/0005-envoi-unique.sql",
|
|
43
43
|
fonction: "empêcher qu'un renvoi crée un second message",
|
|
44
44
|
},
|
|
45
|
+
reactionsOrdonnees: {
|
|
46
|
+
table: "doc_presentation_messages", colonne: "reactions_seq",
|
|
47
|
+
migration: "supabase/migrations/0006-reactions-ordonnees.sql",
|
|
48
|
+
fonction: "empêcher deux réactions simultanées de s'écraser",
|
|
49
|
+
},
|
|
45
50
|
};
|
|
46
51
|
|
|
47
52
|
/**
|
|
@@ -71,6 +76,25 @@ let PLAYER = null;
|
|
|
71
76
|
*/
|
|
72
77
|
const connues = new Map();
|
|
73
78
|
|
|
79
|
+
/**
|
|
80
|
+
* ⚠️ UN « NON » N'A PAS LA MÊME DURÉE DE VIE QU'UN « OUI » — et les confondre a fait qu'une panne
|
|
81
|
+
* passagère ÉTEIGNAIT des fonctions pour la vie du processus.
|
|
82
|
+
*
|
|
83
|
+
* Un « oui » est stable : une colonne présente ne disparaît pas (les migrations sont additives, et
|
|
84
|
+
* personne ne supprime une colonne sous un processus qui tourne). Un « non », lui, recouvre deux
|
|
85
|
+
* réalités que la sonde ne distingue pas — colonne absente, base injoignable — dont la seconde se
|
|
86
|
+
* répare TOUTE SEULE. Retenir ce « non » pour toujours transformait un hoquet de base pendant un
|
|
87
|
+
* envoi de message en idempotence morte jusqu'au redémarrage, sans un mot.
|
|
88
|
+
*
|
|
89
|
+
* D'où : un « oui » est retenu pour le processus ; un « non » expire. Le délai borne le coût chez
|
|
90
|
+
* un hôte réellement non migré (une sonde ratée par minute et par attente, pas une par écriture)
|
|
91
|
+
* tout en rendant la guérison automatique — la seule direction où l'erreur se répare toute seule.
|
|
92
|
+
* Trouvé par le troisième audit, qui a aussi montré que notre essai de récupération appelait
|
|
93
|
+
* `init()` — lequel vide précisément ce cache : il prouvait une guérison qui n'existait pas.
|
|
94
|
+
*/
|
|
95
|
+
const TTL_NEGATIF_MS = 60 * 1000;
|
|
96
|
+
const quandNegatif = new Map(); // cle → instant du « non », pour le faire expirer
|
|
97
|
+
|
|
74
98
|
/**
|
|
75
99
|
* Les réponses DÉJÀ OBTENUES, pour qui veut les lire — la carte d'identité, essentiellement.
|
|
76
100
|
*
|
|
@@ -84,6 +108,9 @@ function init(ctx) {
|
|
|
84
108
|
PLAYER = ctx;
|
|
85
109
|
connues.clear();
|
|
86
110
|
reponses.clear();
|
|
111
|
+
quandNegatif.clear();
|
|
112
|
+
sondeEnCours = null;
|
|
113
|
+
sondePrise = 0;
|
|
87
114
|
}
|
|
88
115
|
|
|
89
116
|
/** La sonde d'une attente déclarée. C'est la seule forme d'appel que les appelants utilisent. */
|
|
@@ -146,6 +173,12 @@ function etatDuSchema() {
|
|
|
146
173
|
*/
|
|
147
174
|
function aLaColonne(table, colonne, migration) {
|
|
148
175
|
const cle = `${table}.${colonne}`;
|
|
176
|
+
// Un « non » périmé n'est plus une réponse : on repose la question. Un « oui » ne périme pas.
|
|
177
|
+
const depuis = quandNegatif.get(cle);
|
|
178
|
+
if (depuis !== undefined && Date.now() - depuis > TTL_NEGATIF_MS) {
|
|
179
|
+
connues.delete(cle);
|
|
180
|
+
quandNegatif.delete(cle);
|
|
181
|
+
}
|
|
149
182
|
if (!connues.has(cle)) connues.set(cle, sonder(table, colonne, migration, cle));
|
|
150
183
|
return connues.get(cle);
|
|
151
184
|
}
|
|
@@ -166,6 +199,7 @@ async function sonder(table, colonne, migration, cle) {
|
|
|
166
199
|
// portage a un fichier à réécrire, pas une habitude à retrouver partout.
|
|
167
200
|
const champ = encodeURIComponent(colonne);
|
|
168
201
|
await PLAYER.db.request(`${table}?select=${champ}&limit=0`);
|
|
202
|
+
quandNegatif.delete(cle);
|
|
169
203
|
noter(cle, true, migration);
|
|
170
204
|
return true;
|
|
171
205
|
} catch {
|
|
@@ -174,6 +208,7 @@ async function sonder(table, colonne, migration, cle) {
|
|
|
174
208
|
// message d'erreur, c'est-à-dire de dépendre du texte d'un service tiers. Ce qui change entre
|
|
175
209
|
// les deux, c'est la durée : une base injoignable le redevient, et le processus suivant reposera
|
|
176
210
|
// la question.
|
|
211
|
+
quandNegatif.set(cle, Date.now());
|
|
177
212
|
noter(cle, false, migration);
|
|
178
213
|
signaler(cle, migration);
|
|
179
214
|
return false;
|
|
@@ -201,7 +236,24 @@ function verdict(manque, sondees, attendues) {
|
|
|
201
236
|
* messages jusqu'au prochain démarrage. Une route de contrôle qui casse la production. Si le
|
|
202
237
|
* témoin ne répond pas, on ne sonde RIEN et on ne retient RIEN.
|
|
203
238
|
*/
|
|
204
|
-
|
|
239
|
+
/**
|
|
240
|
+
* ⚠️ LA SONDE EST PUBLIQUE : chaque appel coûtait des requêtes base, autant de fois qu'on veut.
|
|
241
|
+
* Une boucle sur `?contract=1&schema=1` faisait de la carte un petit amplificateur — la ressource
|
|
242
|
+
* PARTAGÉE paie, pas l'appelant. Deux bornes : les appels simultanés partagent UNE sonde, et le
|
|
243
|
+
* résultat sert pendant 30 s — un état de schéma ne change pas plus vite qu'une migration.
|
|
244
|
+
*/
|
|
245
|
+
const CACHE_SONDE_MS = 30 * 1000;
|
|
246
|
+
let sondeEnCours = null;
|
|
247
|
+
let sondePrise = 0;
|
|
248
|
+
|
|
249
|
+
function sonderTout() {
|
|
250
|
+
if (sondeEnCours && Date.now() - sondePrise < CACHE_SONDE_MS) return sondeEnCours;
|
|
251
|
+
sondePrise = Date.now();
|
|
252
|
+
sondeEnCours = vraimentSonderTout();
|
|
253
|
+
return sondeEnCours;
|
|
254
|
+
}
|
|
255
|
+
|
|
256
|
+
async function vraimentSonderTout() {
|
|
205
257
|
// ⚠️ LA PART ENCODÉE EST CALCULÉE À PART, comme dans `sonder()` dix lignes plus haut, et pour la
|
|
206
258
|
// même raison : la garde de portabilité traque une parenthèse après « select= », et un appel de
|
|
207
259
|
// fonction écrit dans le gabarit en produit une. J'ai reproduit ici le défaut dont le correctif
|
|
@@ -215,6 +267,11 @@ async function sonderTout() {
|
|
|
215
267
|
// dit que cette mesure-ci n'a pas eu lieu. Taire l'un ou l'autre serait mentir d'un côté.
|
|
216
268
|
return { ...etatDuSchema(), verdict: "indetermine" };
|
|
217
269
|
}
|
|
270
|
+
// ⚠️ LE TÉMOIN VIENT DE RÉPONDRE : tout « non » encore en cache est SUSPECT — il peut dater
|
|
271
|
+
// d'une panne guérie. On le jette et on repose la question, sinon ce diagnostic rendrait la
|
|
272
|
+
// valeur d'un incident passé en la datant d'aujourd'hui. Les « oui » restent : ils sont stables.
|
|
273
|
+
for (const [cle] of quandNegatif) { connues.delete(cle); reponses.delete(cle); }
|
|
274
|
+
quandNegatif.clear();
|
|
218
275
|
for (const nom of Object.keys(ATTENDUES)) await attendue(nom);
|
|
219
276
|
return etatDuSchema();
|
|
220
277
|
}
|
|
@@ -238,6 +295,6 @@ function signaler(cle, migration) {
|
|
|
238
295
|
}
|
|
239
296
|
|
|
240
297
|
/** Pour les tests et l'exploitation : reposer la question. */
|
|
241
|
-
function oublier() { connues.clear(); reponses.clear(); }
|
|
298
|
+
function oublier() { connues.clear(); reponses.clear(); quandNegatif.clear(); sondeEnCours = null; sondePrise = 0; }
|
|
242
299
|
|
|
243
300
|
module.exports = { init, aLaColonne, attendue, etatDuSchema, sonderTout, oublier, ATTENDUES, TEMOIN };
|
package/supabase/init.sql
CHANGED
|
@@ -126,7 +126,10 @@ create index if not exists commercial_doc_internal_sessions_last_at_idx
|
|
|
126
126
|
-- que le droit de suivre.
|
|
127
127
|
create table if not exists public.doc_presentations (
|
|
128
128
|
slug text primary key,
|
|
129
|
-
|
|
129
|
+
-- ⚠️ NULLABLE, ET C'EST LE CŒUR DU MODÈLE : null = archivée (plus de pilote). Déclarée NOT NULL
|
|
130
|
+
-- jusqu'à la 0008, ce qui rendait la CLÔTURE impossible sur une base neuve — trouvé par le banc
|
|
131
|
+
-- vraie base, seul endroit où une contrainte peut refuser quelque chose.
|
|
132
|
+
control_hash text,
|
|
130
133
|
doc_id text,
|
|
131
134
|
file_url text not null,
|
|
132
135
|
file_name text,
|
|
@@ -173,7 +176,10 @@ create table if not exists public.doc_presentation_messages (
|
|
|
173
176
|
attachment jsonb,
|
|
174
177
|
-- Clé d'idempotence fabriquée par le client AVANT le premier envoi et réutilisée au renvoi :
|
|
175
178
|
-- un renvoi réseau ne crée pas un second message. Nulle si le client ne la fournit pas.
|
|
176
|
-
client_key text
|
|
179
|
+
client_key text,
|
|
180
|
+
-- Rang de la dernière écriture de réactions acceptée : deux réactions simultanées ne
|
|
181
|
+
-- s'écrasent plus — l'écriture conditionnée à un rang dépassé ne modifie rien, le serveur rejoue.
|
|
182
|
+
reactions_seq bigint not null default 0
|
|
177
183
|
);
|
|
178
184
|
create index if not exists dpm_slug_idx on public.doc_presentation_messages (slug, created_at);
|
|
179
185
|
-- ⚠️ Portée sur (slug, client_key), pas sur la clé seule : deux présentations n'ont aucune raison
|
|
@@ -310,15 +316,18 @@ alter table public.doc_bot_sessions enable row level security;
|
|
|
310
316
|
-- le présentateur sans qu'aucune table ne soit lisible publiquement. C'est ce qui permet de ne
|
|
311
317
|
-- créer aucune politique de lecture au-dessus.
|
|
312
318
|
--
|
|
313
|
-
-- ⚠️ LE CHAT
|
|
314
|
-
--
|
|
315
|
-
--
|
|
316
|
-
--
|
|
319
|
+
-- ⚠️ LE CHAT AUSSI EST SUR CE CHEMIN, DÉSORMAIS. Un message émet un signal broadcast (`msg`,
|
|
320
|
+
-- `msg-upd`) et chaque participant RELIT l'historique par la route du player — le signal dit
|
|
321
|
+
-- « quelque chose a changé », jamais le contenu. Aucune politique de lecture n'est donc
|
|
322
|
+
-- nécessaire, et le chat est en direct sur une installation neuve.
|
|
317
323
|
--
|
|
318
|
-
--
|
|
319
|
-
--
|
|
320
|
-
--
|
|
321
|
-
--
|
|
324
|
+
-- (Ce commentaire a affirmé le contraire pendant plusieurs versions après le portage du chat en
|
|
325
|
+
-- broadcast — le troisième audit l'a relevé. Un commentaire périmé dans un fichier d'installation
|
|
326
|
+
-- n'est pas un détail : il pousse un mainteneur à « réparer » un différé qui n'existe plus, en
|
|
327
|
+
-- ajoutant précisément la politique de lecture que ce fichier refuse.)
|
|
328
|
+
--
|
|
329
|
+
-- La publication de la table reste utile aux INSTANCES HISTORIQUES encore sur `postgres_changes`,
|
|
330
|
+
-- et inoffensive ici : publiée sans politique de lecture, elle ne livre rien à personne.
|
|
322
331
|
--
|
|
323
332
|
-- ⚠️ Ne mettez de toute façon jamais de donnée confidentielle dans un message de chat ni dans un
|
|
324
333
|
-- nom d'auteur.
|
|
@@ -336,6 +345,43 @@ begin
|
|
|
336
345
|
end $$;
|
|
337
346
|
alter table public.doc_presentation_messages replica identity full;
|
|
338
347
|
|
|
348
|
+
-- ── Le scellé de l'archive ─────────────────────────────────────────────────────────────────────
|
|
349
|
+
-- Sept chemins d'écriture vérifient l'archive PUIS écrivent — dans une AUTRE table que celle qui
|
|
350
|
+
-- porte l'état. Aucun filtre PostgREST ne peut fermer cette fenêtre : l'arbitre est la base.
|
|
351
|
+
-- `FOR KEY SHARE` bloque une clôture concurrente jusqu'au commit de l'écriture — c'est lui qui
|
|
352
|
+
-- rend le refus atomique, pas le simple test.
|
|
353
|
+
create or replace function public.player_archive_scellee()
|
|
354
|
+
returns trigger
|
|
355
|
+
language plpgsql
|
|
356
|
+
as $$
|
|
357
|
+
declare
|
|
358
|
+
v_active boolean;
|
|
359
|
+
v_control text;
|
|
360
|
+
begin
|
|
361
|
+
select p.active, p.control_hash into v_active, v_control
|
|
362
|
+
from public.doc_presentations p
|
|
363
|
+
where p.slug = new.slug
|
|
364
|
+
for key share;
|
|
365
|
+
-- Pas de présentation : rien à sceller (chat d'un lien sans session live, données historiques).
|
|
366
|
+
if not found then return new; end if;
|
|
367
|
+
if v_active = false and v_control is null then
|
|
368
|
+
raise exception 'presentation archivee : ecriture refusee'
|
|
369
|
+
using errcode = 'P0001', hint = 'l''archive est en lecture seule';
|
|
370
|
+
end if;
|
|
371
|
+
return new;
|
|
372
|
+
end
|
|
373
|
+
$$;
|
|
374
|
+
|
|
375
|
+
drop trigger if exists dpm_archive_scellee on public.doc_presentation_messages;
|
|
376
|
+
create trigger dpm_archive_scellee
|
|
377
|
+
before insert or update on public.doc_presentation_messages
|
|
378
|
+
for each row execute function public.player_archive_scellee();
|
|
379
|
+
|
|
380
|
+
drop trigger if exists dpa_archive_scellee on public.doc_presentation_attendees;
|
|
381
|
+
create trigger dpa_archive_scellee
|
|
382
|
+
before insert or update on public.doc_presentation_attendees
|
|
383
|
+
for each row execute function public.player_archive_scellee();
|
|
384
|
+
|
|
339
385
|
-- ── Rattrapage : bases installées depuis un init.sql plus ancien ───────────────────────────────
|
|
340
386
|
--
|
|
341
387
|
-- ⚠️ CE FICHIER A ÉTÉ INCOMPLET, ET RIEN NE LE DISAIT. Il annonçait « un seul fichier, sans rien à
|
|
@@ -355,3 +401,8 @@ alter table public.doc_presentations
|
|
|
355
401
|
add column if not exists write_seq bigint not null default 0;
|
|
356
402
|
alter table public.doc_presentation_messages
|
|
357
403
|
add column if not exists client_key text;
|
|
404
|
+
-- Une base née d'un init.sql d'avant la 0008 porte un NOT NULL qui rend la clôture impossible.
|
|
405
|
+
alter table public.doc_presentations
|
|
406
|
+
alter column control_hash drop not null;
|
|
407
|
+
alter table public.doc_presentation_messages
|
|
408
|
+
add column if not exists reactions_seq bigint not null default 0;
|
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
-- DEUX RÉACTIONS SIMULTANÉES S'ÉCRASAIENT.
|
|
2
|
+
--
|
|
3
|
+
-- Le serveur lit le JSON des réactions, le modifie en mémoire, le réécrit en entier : deux
|
|
4
|
+
-- participants qui réagissent au même message dans la même seconde lisent le même état, et la
|
|
5
|
+
-- seconde écriture emporte la première — une réaction disparaît, sans erreur nulle part.
|
|
6
|
+
--
|
|
7
|
+
-- Même remède que pour les écritures de pilotage (0002) : un rang. L'écriture porte le rang
|
|
8
|
+
-- qu'elle a lu et n'est acceptée que s'il n'a pas bougé ; sinon le serveur relit et rejoue.
|
|
9
|
+
-- La comparaison et l'écriture deviennent le même geste — c'est la base qui arbitre, pas la
|
|
10
|
+
-- mémoire d'un processus.
|
|
11
|
+
alter table public.doc_presentation_messages
|
|
12
|
+
add column if not exists reactions_seq bigint not null default 0;
|
|
13
|
+
|
|
14
|
+
comment on column public.doc_presentation_messages.reactions_seq is
|
|
15
|
+
'Rang de la dernière écriture de réactions acceptée. Une écriture conditionnée à un rang déjà '
|
|
16
|
+
'dépassé ne modifie aucune ligne : le serveur relit et rejoue. Sans lui : rien ne casse, mais '
|
|
17
|
+
'deux réactions simultanées peuvent s''écraser (dernier écrivain gagne), comme avant la 0006.';
|
|
@@ -0,0 +1,49 @@
|
|
|
1
|
+
-- L'ARCHIVE ÉTAIT VÉRIFIÉE, PUIS L'ÉCRITURE PARTAIT SANS ELLE.
|
|
2
|
+
--
|
|
3
|
+
-- Sept chemins d'écriture (message, édition, suppression, réaction, présence, verrou, pièce
|
|
4
|
+
-- jointe) demandent d'abord si la présentation est archivée, puis écrivent. Entre les deux, une
|
|
5
|
+
-- clôture peut passer : un message atterrit dans une archive « en lecture seule ». La fenêtre est
|
|
6
|
+
-- étroite, mais la promesse — une archive ne bouge plus — était tenue par du code, pas par la base.
|
|
7
|
+
--
|
|
8
|
+
-- ⚠️ AUCUN FILTRE POSTGREST NE PEUT FERMER ÇA : les écritures visent doc_presentation_messages et
|
|
9
|
+
-- doc_presentation_attendees, la condition vit dans doc_presentations — une AUTRE table. Sans
|
|
10
|
+
-- transaction ni RPC côté player, l'arbitre ne peut être que la base elle-même. D'où ce trigger.
|
|
11
|
+
--
|
|
12
|
+
-- ⚠️ `FOR KEY SHARE` EST CE QUI REND LE REFUS ATOMIQUE, pas le simple test. Sans lui, une clôture
|
|
13
|
+
-- peut se COMMITTER entre la vérification du trigger et le commit de l'écriture — la fenêtre
|
|
14
|
+
-- rétrécit mais survit. Le verrou de ligne bloque la clôture concurrente jusqu'au commit de
|
|
15
|
+
-- l'écriture : l'ordre devient réel, plus probable.
|
|
16
|
+
--
|
|
17
|
+
-- Sans lui : rien ne casse — les sept chemins gardent leur vérification préalable, et la fenêtre
|
|
18
|
+
-- de course reste ce qu'elle était avant cette migration. Le trigger est le scellé, pas la porte.
|
|
19
|
+
create or replace function public.player_archive_scellee()
|
|
20
|
+
returns trigger
|
|
21
|
+
language plpgsql
|
|
22
|
+
as $$
|
|
23
|
+
declare
|
|
24
|
+
v_active boolean;
|
|
25
|
+
v_control text;
|
|
26
|
+
begin
|
|
27
|
+
select p.active, p.control_hash into v_active, v_control
|
|
28
|
+
from public.doc_presentations p
|
|
29
|
+
where p.slug = new.slug
|
|
30
|
+
for key share;
|
|
31
|
+
-- Pas de présentation : rien à sceller (chat d'un lien sans session live, données historiques).
|
|
32
|
+
if not found then return new; end if;
|
|
33
|
+
if v_active = false and v_control is null then
|
|
34
|
+
raise exception 'presentation archivee : ecriture refusee'
|
|
35
|
+
using errcode = 'P0001', hint = 'l''archive est en lecture seule';
|
|
36
|
+
end if;
|
|
37
|
+
return new;
|
|
38
|
+
end
|
|
39
|
+
$$;
|
|
40
|
+
|
|
41
|
+
drop trigger if exists dpm_archive_scellee on public.doc_presentation_messages;
|
|
42
|
+
create trigger dpm_archive_scellee
|
|
43
|
+
before insert or update on public.doc_presentation_messages
|
|
44
|
+
for each row execute function public.player_archive_scellee();
|
|
45
|
+
|
|
46
|
+
drop trigger if exists dpa_archive_scellee on public.doc_presentation_attendees;
|
|
47
|
+
create trigger dpa_archive_scellee
|
|
48
|
+
before insert or update on public.doc_presentation_attendees
|
|
49
|
+
for each row execute function public.player_archive_scellee();
|
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
-- CLÔTURER ÉTAIT IMPOSSIBLE SUR UNE BASE NEUVE.
|
|
2
|
+
--
|
|
3
|
+
-- La clôture d'une présentation pose `control_hash = null` : c'est LE marqueur d'archive — le slug
|
|
4
|
+
-- seul donne le droit de suivre, le hash celui de piloter, et une archive n'a plus de pilote.
|
|
5
|
+
-- Or init.sql déclarait la colonne NOT NULL : chaque clôture violait la contrainte (23502) et
|
|
6
|
+
-- toute présentation restait « active » pour toujours chez un hôte installé depuis ce fichier.
|
|
7
|
+
--
|
|
8
|
+
-- ⚠️ TROUVÉ PAR LE BANC « VRAIE BASE », PAS PAR UN LECTEUR : 793 essais passaient — le double en
|
|
9
|
+
-- mémoire n'a pas de contraintes, c'est écrit dans son en-tête. Les bases HISTORIQUES, nées de
|
|
10
|
+
-- migrations plus anciennes, sont nullables : la production ne pouvait pas le montrer non plus.
|
|
11
|
+
--
|
|
12
|
+
-- Sans lui : sur une base née d'un init.sql d'avant cette correction, `endPresentation` échoue en
|
|
13
|
+
-- erreur serveur et l'archive en lecture seule n'existe pas — les présentations ne se ferment pas.
|
|
14
|
+
alter table public.doc_presentations
|
|
15
|
+
alter column control_hash drop not null;
|
|
16
|
+
|
|
17
|
+
comment on column public.doc_presentations.control_hash is
|
|
18
|
+
'Hash du jeton de pilotage. NULL = présentation archivée : plus personne ne pilote, et le '
|
|
19
|
+
'scellé (0007) refuse toute écriture. Le slug seul ne donne que le droit de suivre.';
|