PageSourceSearch

https://creaconnect.world/src/crea-accords.js

js creaconnect.world collected 2026-09-24 10:09:29 UTC 27,597 bytes, 537 lines download raw bytes

1// LA FILE DES DEMANDES D'AUTORISATION — UNE FENTE NE SUFFIT PAS
2//
3// « Autoriser Claude à exécuter … ? » avec la commande sous les yeux, et trois
4// réponses : Refuser, Autoriser une fois, Toujours autoriser. Ce bloc existe
5// depuis longtemps et il est juste. Ce qui manquait est ce qui se passe QUAND
6// PERSONNE N'EST DEVANT — c'est-à-dire à 4 h 06, l'heure à laquelle une cadence
7// travaille.
8//
9// ── TROIS DÉFAUTS, ET ILS N'EN FONT QU'UN ─────────────────────────────────
10//
11//   UNE SEULE FENTE. `STATE.accordEnAttente` est un emplacement unique. Deux
12//   cadences qui partent ensemble : la seconde ÉCRASE la première, dont la
13//   promesse n'est alors jamais résolue. La première boucle reste suspendue
14//   jusqu'à ce que le serveur abandonne, et personne ne saura pourquoi.
15//
16//   AUCUN DÉLAI CÔTÉ CLIENT. Le serveur, lui, renonce au bout de deux minutes
17//   (`TIMEOUT_ACCORD_MS`) et poursuit en « refusé ». Le bloc, lui, restait
18//   affiché POUR TOUJOURS : à 8 h on trouvait une demande orpheline pour une
19//   exécution finie depuis longtemps — et y répondre ne servait plus à rien.
20//
21//   RIEN N'EN ÉTAIT ÉCRIT. Le journal disait « échouée », jamais « elle a
22//   demandé la permission d'exécuter X à 4 h 06, personne n'a répondu ». Un
23//   échec sans cause se reproduit.
24//
25// ── POURQUOI CE MORCEAU EXISTE ────────────────────────────────────────────
26//
27// Les deux cliquets des portées étaient à zéro marge : `src/app.js` ne pouvait
28// pas gagner une ligne. La règle du dépôt dit où va ce qui s'écrit de neuf —
29// dans un morceau, pas dans le tronc. Ce fichier prend donc la MACHINE À ÉTATS
30// des accords ; le tronc garde ce qui dessine, et délègue.
31//
32// TOUT PASSE PAR `window.` ET C'EST DÉLIBÉRÉ : le relevé des portées ne compte
33// que les identifiants NUS. Emprunter `STATE` ou `render` par leur nom créerait
34// des liens implicites qu'il faudrait payer en lignes de tronc ; les lire sur
35// `window` n'en crée aucun, et dit à la lecture que ce morceau ne s'appuie sur
36// rien qu'il ne nomme.
37;(function () {
38  function etat() { try { return window.STATE || {} } catch (e) { return {} } }
39  function peindre() {
40    try { if (typeof window.render === 'function') window.render() } catch (e) { /* hors application */ }
41  }
42
43  /* LE DÉLAI DU CLIENT EST CELUI DU SERVEUR, MOINS UNE SECONDE.
44     `TIMEOUT_ACCORD_MS` vaut deux minutes dans server/agent-boucle-route.js. Y
45     répondre APRÈS que le serveur a renoncé est pire que ne pas répondre : on
46     croit avoir autorisé, et rien ne part. La marge fait que le bloc disparaît
47     avant que la réponse ne devienne inutile, jamais après. */
48  var DELAI_MS = 2 * 60 * 1000
49  var MARGE_MS = 1000
50  function delai() { return Math.max(1000, DELAI_MS - MARGE_MS) }
51  window.accordsDelaiMs = delai
52
53  /* CE QUI ATTEND SON TOUR. La demande AFFICHÉE vit dans `STATE.accordEnAttente`
54     — le tronc la dessine déjà, et lui prendre sa place aurait voulu dire
55     réécrire l'écran. Les suivantes patientent ici. */
56  var file = []
57
58  /* LA PATIENCE D'UNE DEMANDE QUI PART AILLEURS N'EST PAS CELLE D'UN CLIC.
59     Deux minutes suffisent devant l'écran : on lit, on tranche. Publiée vers un
60     téléphone, la même demande doit survivre au trajet — la notification, le
61     déverrouillage, l'ouverture de l'application. Deux minutes la feraient
62     expirer pendant que la personne la sort de sa poche, et l'outil ne
63     partirait pas alors qu'elle venait dire oui. */
64  var DELAI_DISTANT_MS = 10 * 60 * 1000
65  window.accordsDelaiDistantMs = function () { return DELAI_DISTANT_MS }
66
67  function minuter(a, patience) {
68    var d = Math.max(1000, Number(patience) || delai())
69    a._fin = Date.now() + d
70    a._minuteur = setTimeout(function () { expirer(a) }, d)
71  }
72  function desarmer(a) {
73    if (a && a._minuteur) { clearTimeout(a._minuteur); a._minuteur = null }
74  }
75
76  function rendre(a, choix) {
77    desarmer(a)
78    try { a._tranche(choix) } catch (e) {
79      /* Déjà tranchée — un double clic, ou une touche pendant le clic. Le
80         premier choix fait foi ; le second n'a rien à annuler. */
81    }
82  }
83
84  /* LA SUIVANTE PREND LA PLACE. Sans ça, une file se remplirait sans jamais se
85     vider : on répondrait à la première et les autres attendraient leur propre
86     expiration, en silence. */
87  function avancer() {
88    var S = etat()
89    S.accordEnAttente = null
90    var suivante = null
91    while (file.length && !suivante) {
92      var c = file.shift()
93      if (c && !c._rendue) suivante = c
94    }
95    if (suivante) S.accordEnAttente = suivante
96    peindre()
97    if (suivante) amener()
98  }
99
100  function amener() {
101    /* On amène le bloc sous les yeux : il est au-dessus de la saisie, donc en
102       bas du fil — et si la conversation était remontée, il arriverait hors
103       champ. Une demande qu'on ne voit pas est une demande qui n'a pas été
104       posée. */
105    if (typeof setTimeout !== 'function' || typeof document === 'undefined') return
106    setTimeout(function () {
107      try { var n = document.getElementById('crea-accord'); if (n && n.scrollIntoView) n.scrollIntoView({ block: 'nearest' }) } catch (e) {
108        /* Sans mise en page, il n'y a rien à faire défiler : le bloc est là,
109           c'est ce qui compte. Lever ici empêcherait la demande d'exister. */
110      }
111    }, 0)
112  }
113
114  /* CE QUI ARRIVE QUAND PERSONNE NE RÉPOND. On rend « expire », PAS « refus » :
115     les deux empêchent l'outil, mais un refus est une décision et une
116     expiration est une absence. Confondre les deux ferait écrire au journal que
117     quelqu'un a refusé à 4 h 06. */
118  function expirer(a) {
119    if (!a || a._rendue) return
120    a._rendue = true
121    var S = etat()
122    if (a.origine && typeof window.autoworkAccordExpire === 'function') {
123      try { window.autoworkAccordExpire(a.origine, a.nom, Date.now()) } catch (e) { /* la trace ne doit pas empêcher la sortie */ }
124    }
125    rendre(a, 'expire')
126    if (S.accordEnAttente === a) avancer()
127    else peindre()
128  }
129
130  /* POSER UNE DEMANDE. Rend une promesse qui se résout en 'refus', 'une-fois',
131     'toujours' ou 'expire'. */
132  window.accordsPoser = function (nom, entree, contexte, origine) {
133    return new Promise(function (resolve) {
134      var S = etat()
135      var a = {
136        nom: nom, entree: entree, contexte: contexte || '',
137        origine: origine || null, at: Date.now(), _tranche: resolve, _rendue: false,
138      }
139      /* LE MINUTEUR PART TOUT DE SUITE, MÊME EN FILE. Le serveur, lui, ne fait
140         pas la queue : sa patience a commencé à courir au moment où il a
141         demandé. Une demande qui attendrait son tour sans compter obtiendrait
142         un délai plus long que celui qui la rend encore utile. */
143      /* ── TROIS PATIENCES, ET LES CONFONDRE FAIT PERDRE LA RÉPONSE ──────
144       *
145       *   devant l'écran — deux minutes : on lit, on tranche.
146       *   publiée         — dix minutes : le temps du trajet vers un téléphone.
147       *   relevée d'un
148       *   autre poste     — CE QU'IL RESTE À LA SIENNE, et pas une seconde de
149       *                     plus ni de moins. Réglée sur deux minutes, elle
150       *                     disparaissait de cet écran huit minutes avant que
151       *                     le poste renonce — et `connues` l'empêchait de
152       *                     revenir. La question était perdue pour tout le
153       *                     monde alors que personne n'avait renoncé. */
154      var patience = null
155      if (origine && origine.expireA) patience = Number(origine.expireA) - Date.now()
156      else if (origine && origine.dispatch) patience = DELAI_DISTANT_MS
157      minuter(a, patience)
158      if (origine && origine.dispatch) publier(a)
159      if (!S.accordEnAttente) { S.accordEnAttente = a; peindre(); amener() }
160      else file.push(a)
161    })
162  }
163
164  window.accordsTrancher = function (choix) {
165    var S = etat()
166    var a = S.accordEnAttente
167    if (!a) return false
168    a._rendue = true
169    /* RÉPONDRE ICI ÉTEINT LA QUESTION PARTOUT. Quelqu'un est passé devant ce
170       poste et a tranché ; sans ce mot au serveur, la même question resterait
171       affichée sur le téléphone pendant les dix minutes de sa patience — on
172       cliquerait « Autoriser » sur une décision déjà prise, et rien ne s'en
173       apercevrait. `_publiee` n'existe que sur une demande qui est partie. */
174    if (a._publiee) repondreAuServeur(a._publiee, choix)
175    if (choix === 'toujours' && typeof window.poserAccordToujours === 'function') {
176      try { window.poserAccordToujours(a.nom) } catch (e) { /* le refus d'écrire ne doit pas bloquer l'action */ }
177    }
178    rendre(a, choix)
179    avancer()
180    return true
181  }
182
183  /* ══ UNE DEMANDE NE S'AFFICHE QUE CHEZ ELLE ══════════════════════════════
184   *
185   * « Je vais être dans Work, il va m'apparaître une demande d'autorisation
186   *  pour une session qui... normalement c'est dans sa session que ça se passe.
187   *  Ce n'est pas normal. »
188   *
189   * C'est exact, et c'était structurel. `STATE.accordEnAttente` est un
190   * emplacement GLOBAL, et le bloc se dessine au-dessus de la barre de
191   * saisie — barre qui est rendue sur TOUTES les surfaces. Une cadence de
192   * 4 h 06 posait donc sa question sur l'écran de Work, du chat, de n'importe
193   * où. Et le danger n'est pas le dérangement : c'est qu'on AUTORISE en croyant
194   * que la demande porte sur ce qu'on est en train de faire.
195   *
196   * LE FOYER D'UNE DEMANDE EST CELUI DE SON TRAVAIL. Il voyage dans `origine`,
197   * que la boucle de fond remplit déjà : le studio et la session visés, ou la
198   * conversation. Une demande née de l'écran n'a pas de foyer, et c'est juste :
199   * elle appartient à l'endroit où l'on est, quel qu'il soit.
200   *
201   * ET AILLEURS, ON NE MENT PAS PAR OMISSION. Une demande invisible qui expire
202   * en silence serait pire que mal placée. `accordsAilleurs` dit qu'il y en a
203   * une et où — la section « Cadences » de la barre s'en sert pour marquer la
204   * ligne qui attend. */
205  function memeFoyer(a) {
206    var o = a && a.origine
207    /* SANS FOYER, ELLE EST CHEZ ELLE PARTOUT — SAUF SI ELLE VIENT D'UNE
208       CADENCE, ET C'ÉTAIT MON ERREUR.
209       « Ça continue à m'envoyer des demandes d'autorisation partout où je me
210       trouve. » J'avais fait dépendre « née de l'écran » de l'ABSENCE de
211       `sourceMode`. Or une cadence qui travaille EN FOND n'en a pas non plus :
212       elle passait donc par la porte prévue pour les demandes de l'écran, et
213       s'affichait partout. Le discriminant n'est pas l'absence de foyer, c'est
214       `auto` — une demande qui porte une cadence n'est JAMAIS née de l'écran.
215       Elle attend dans sa session si elle en a une, et nulle part ailleurs :
216       le journal en garde la trace, la section « Cadences » la signale, et la
217       vraie réponse est de régler ses autorisations d'avance — c'est à ça que
218       sert le champ du formulaire. */
219    if (!o) return true
220    if (o.auto) return !!(o.sourceMode || o.convId) && foyerVise(o)
221    if (!o.sourceMode && !o.convId) return true
222    return foyerVise(o)
223  }
224  function foyerVise(o) {
225    var S = etat()
226    if (o.convId) return S.activeId === o.convId
227    if (o.sourceMode === 'chat') return !S.workMode || S.workMode === 'chat'
228    if (S.workMode !== o.sourceMode) return false
229    /* LE STUDIO NE SUFFIT PAS : deux sessions de Code sont deux endroits. Sans
230       session nommée, la demande vient du studio lui-même et s'y affiche. */
231    if (!o.sessionId) return true
232    return ((S.activeStudioSession || {})[o.sourceMode]) === o.sessionId
233  }
234  /* ── ET LA FENTE SUIT L'ÉCRAN, PAS L'ORDRE D'ARRIVÉE ─────────────────────
235   *
236   * Une seule demande s'affiche à la fois. Si celle qui occupe la fente
237   * appartient à une autre session, elle bloquerait TOUTES les autres : on
238   * arriverait dans la session qui attend vraiment, et on n'y verrait rien.
239   *
240   * On échange donc les deux. Pas de peinture ici : `blocEnAttente()` appelle
241   * cette fonction JUSTE AVANT que le tronc lise `STATE.accordEnAttente`, et
242   * la même passe de rendu dessine la bonne. Repeindre depuis un rendu
243   * bouclerait. */
244  window.accordsIci = function () {
245    var S = etat()
246    var a = S.accordEnAttente
247    if (!a) return false
248    if (memeFoyer(a)) return true
249    var i = -1
250    for (var k = 0; k < file.length; k++) if (file[k] && !file[k]._rendue && memeFoyer(file[k])) { i = k; break }
251    if (i < 0) return false
252    S.accordEnAttente = file[i]
253    file[i] = a
254    return true
255  }
256
257  /* LE MÊME JUGEMENT, POUR LE PLAN ET LA QUESTION.
258     Ces deux-là n'ont pas de file — une seule fente, pas d'échange à faire.
259     Mais ils se posent sur la MÊME barre, avec les MÊMES boutons, et une
260     cadence qui demande à valider un plan à 4 h 06 le posait sur tous les
261     écrans exactement comme une autorisation. Le tronc leur donne maintenant
262     une `origine`, et la lit par cette porte. */
263  window.accordsFoyer = function (origine) { return memeFoyer({ origine: origine }) }
264
265  /* CE QUI ATTEND AILLEURS — pour que la barre puisse le marquer. Rend la liste
266     des cadences dont une demande dort hors de l'écran courant.
267     LE PLAN ET LA QUESTION EN SONT, sans quoi la correction ci-dessus ferait
268     disparaître un bloc sans que rien ne dise où il est parti. */
269  /* CE QU'UNE ENTRÉE PORTE, ET POURQUOI PLUS QU'AVANT. La barre n'avait besoin
270     que de `auto` et `nom` pour poser sa pastille. L'accueil des Studios, lui,
271     doit DIRE la rangée : de quel lieu vient l'attente, quelle session, et de
272     quel genre elle est — « une autorisation » et « une question » ne se
273     répondent pas au même endroit. Ce sont des champs de plus sur la même
274     entrée, pas une seconde lecture de la file : deux lectures divergeraient. */
275  function ailleurs(genre, nom, o) {
276    return {
277      genre: genre, nom: nom,
278      auto: o.auto || null, tache: o.tache || null,
279      sourceMode: o.sourceMode || null, sessionId: o.sessionId || null, convId: o.convId || null,
280    }
281  }
282  window.accordsAilleurs = function () {
283    var S = etat()
284    var toutes = (S.accordEnAttente ? [S.accordEnAttente] : []).c
284oncat(file)
285    var out = []
286    toutes.forEach(function (a) {
287      if (!a || a._rendue) return
288      if (S.accordEnAttente === a && memeFoyer(a)) return
289      out.push(ailleurs('accord', a.nom, a.origine || {}))
290    })
291    ;[[S.planEnAttente, 'plan', 'un plan à valider'], [S.questionEnAttente, 'question', 'une question']].forEach(function (p) {
292      if (!p[0] || memeFoyer(p[0])) return
293      out.push(ailleurs(p[1], p[2], p[0].origine || {}))
294    })
295    return out
296  }
297
298  /* ── CE QUE LE BLOC NE DISAIT PAS ───────────────────────────────────────
299     Un délai qui court sans se voir est un piège : on lit la demande, on va
300     chercher la commande ailleurs, on revient, et le bloc a disparu sans qu'on
301     sache pourquoi. Et une demande qui en cache deux autres laisse croire
302     qu'on a fini quand on a répondu à celle-là.
303     LE TRONC N'EN TIENT QU'UNE PORTE — une ligne dans son gabarit. La phrase,
304     l'heure et le compte vivent ici, avec l'horloge qui les décide. */
305  function deuxChiffres(n) { return (n < 10 ? '0' : '') + n }
306  window.accordsBandeau = function (a) {
307    if (!a || !a._fin) return ''
308    var d = new Date(a._fin)
309    var reste = file.filter(function (c) { return c && !c._rendue }).length
310    return '<p class="crea-accord-delai">' +
311      'Sans réponse d\'ici ' + deuxChiffres(d.getHours()) + ' h ' + deuxChiffres(d.getMinutes()) +
312      ', la demande expire et l\'outil ne partira pas.' +
313      (reste ? '<span>' + reste + (reste > 1 ? ' autres demandes attendent' : ' autre demande attend') + '</span>' : '') +
314      '</p>'
315  }
316
317  /* CE QUE LA FILE PORTE — pour l'écran, et pour les gardes. Une file qu'on ne
318     peut pas regarder est une file dont personne ne sait qu'elle déborde. */
319  /* CE QU'ELLE PORTE ENCORE, PAS CE QU'ELLE A PORTÉ. Une demande qui expire
320     sans avoir jamais été affichée reste dans le tableau, marquée rendue :
321     compter la longueur brute annoncerait « 2 autres attendent » quand plus
322     personne n'attend. */
323  window.accordsEnFile = function () {
324    return file.filter(function (c) { return c && !c._rendue }).length
325  }
326  window.accordsVider = function () {
327    var S = etat()
328    var toutes = (S.accordEnAttente ? [S.accordEnAttente] : []).concat(file)
329    file = []
330    S.accordEnAttente = null
331    toutes.forEach(function (a) { if (a && !a._rendue) { a._rendue = true; rendre(a, 'expire') } })
332    peindre()
333  }
334
335  /* ══ L'AUTORISATION SUIT LA PERSONNE, PAS LA MACHINE ═════════════════════
336   *
337   * ── CE QUI SE PASSAIT ────────────────────────────────────────────────────
338   *
339   * On confie un travail depuis le téléphone. Il part sur le Mac du bureau.
340   * Là-bas, l'agent touche un outil sensible et pose sa question — sur un
341   * écran que personne ne regarde. Le minuteur court, la demande expire, et
342   * l'outil ne part pas. La personne, elle, reçoit un compte rendu où CRÉA
343   * explique qu'elle n'a pas pu faire ce qu'on lui demandait. Elle n'a jamais
344   * vu la question.
345   *
346   * ── LA FORME QU'ON N'A PAS PRISE ─────────────────────────────────────────
347   *
348   * Un écran de plus dans le Dispatch, avec ses boutons et son clavier. Il
349   * aurait fallu réécrire ce que ce fichier tient déjà : la file, l'ordre, le
350   * compte à rebours, l'échange de fente, la trace au journal. Deux blocs
351   * d'autorisation pour une même question auraient fini par ne plus dire la
352   * même chose — et c'est sur celui-là qu'on clique « Toujours autoriser ».
353   *
354   * UNE DEMANDE DISTANTE EST UNE DEMANDE ORDINAIRE. Elle passe par
355   * `accordsPoser`, s'affiche dans le même bloc, se tranche au même clavier.
356   * La SEULE différence est où va la réponse : sur le poste elle résout une
357   * promesse, ici elle repart par le réseau. Tout le reste est déjà écrit.
358   *
359   * ── CE QUE ÇA NE TIENT PAS, ET CE QUE ÇA COÛTE ───────────────────────────
360   *
361   * Rien ne garantit que le poste relise la réponse. S'il s'est éteint
362   * entre-temps, on aura tranché dans le vide — et c'est pour ça que la réponse
363   * ne dit jamais « c'est fait », seulement « c'est répondu ».
364   *
365   * ET L'ENTRÉE DE L'OUTIL PASSE PAR LE SERVEUR. Une commande, un chemin, ce
366   * qu'on s'apprête à écrire : ça se pose dans un fichier du compte pendant la
367   * demi-heure où la question peut encore vivre. C'est un prix, et il est
368   * assumé — sans l'aperçu, « Autoriser system_run_command ? » est un chèque en
369   * blanc, et on cliquerait oui sans savoir sur quoi. */
370  var publiees = {}
371  var releveEnCours = false
372  var connues = {}
373
374  function api(methode, chemin, corps) {
375    if (typeof fetch !== 'function') return Promise.resolve(null)
376    return fetch(chemin, {
377      method: methode,
378      headers: corps ? { 'content-type': 'application/json' } : undefined,
379      credentials: 'same-origin',
380      body: corps ? JSON.stringify(corps) : undefined,
381    }).then(function (r) { return r.json() }).catch(function (e) {
382      /* LE RÉSEAU QUI TOMBE NE DOIT PAS EMPORTER LA DEMANDE. Sans réponse
383         distante, le bloc local reste : quelqu'un devant ce poste peut encore
384         trancher, et sinon l'expiration fait son travail comme avant. */
385      return null
386    })
387  }
388
389  /* CE QUE LA QUESTION EMPORTE. L'entrée de l'outil est bornée puis rendue
390     lisible : « execute cette commande » sans la commande n'est pas une
391     question, c'est une case à cocher. */
392  function apercuDe(entree) {
393    if (entree == null) return ''
394    if (typeof entree === 'string') return entree.slice(0, 1200)
395    try { return JSON.stringify(entree).slice(0, 1200) } catch (e) { return '' }
396  }
397
398  /* ON DIT QUI DEMANDE PAR SON IDENTIFIANT, PAS PAR SON NOM. Le libellé du
399     poste vit dans le registre du serveur — c'est celui que la liste des postes
400     affiche. Le recopier ici en ferait une seconde source, et le jour où les
401     deux divergent, la question dit « Mac du bureau » pour une machine qui
402     s'appelle autrement dans la liste. Devant un bouton « Autoriser », c'est la
403     seule chose qu'on n'a pas le droit de se tromper. */
404  function publier(a) {
405    var moi = null
406    try { moi = (window.STATE && window.STATE.dispatchNodeId) || null } catch (e) { moi = null }
407    api('POST', '/api/dispatch/accords', {
408      nom: a.nom, contexte: a.contexte, apercu: apercuDe(a.entree),
409      nodeId: moi, expireA: a._fin,
410    }).then(function (d) {
411      if (!d || !d.ok || !d.accord) return
412      a._publiee = d.accord.id
413      publiees[d.accord.id] = true
414      suivreLaReponse(a)
415    })
416  }
417
418  /* DIRE SON VERDICT AU SERVEUR. Le même geste des deux côtés : ici parce qu'on
419     a tranché devant le poste, là-bas parce qu'on a tranché sur le téléphone. */
420  function repondreAuServeur(id, choix) {
421    if (choix !== 'refus' && choix !== 'une-fois' && choix !== 'toujours') return
422    api('POST', '/api/dispatch/accords/' + encodeURIComponent(id) + '/reponse', { choix: choix })
423  }
424
425  /* ON RELIT SA PROPRE DEMANDE JUSQU'À CE QU'ELLE SOIT TRANCHÉE OU RENDUE. Le
426     serveur ne rappelle personne : sans ce relevé, la réponse du téléphone
427     resterait dans un fichier que ce poste ne lit jamais. */
428  var PAS_MS = 3000
429  function suivreLaReponse(a) {
430    if (!a._publiee || a._rendue) return
431    setTimeout(function () {
432      if (a._rendue) return
433      api('GET', '/api/dispatch/accords').then(function (d) {
434        if (a._rendue) return
435        var mien = ((d && d.accords) || []).filter(function (x) { return x && x.id === a._publiee })[0]
436        if (mien && mien.choix) {
437          a._rendue = true
438          if (mien.choix === 'toujours' && typeof window.poserAccordToujours === 'function') {
439            try { window.poserAccordToujours(a.nom) } catch (e) { /* le refus d'écrire ne doit pas bloquer l'action */ }
440          }
441          rendre(a, mien.choix)
442          var S = etat()
443          if (S.accordEnAttente === a) avancer(); else peindre()
444          return
445        }
446        suivreLaReponse(a)
447      })
448    }, PAS_MS)
449  }
450
451  /* ── ET DE L'AUTRE CÔTÉ : LES QUESTIONS DES AUTRES POSTES ────────────────
452   *
453   * On ne montre pas celles qu'on a publiées soi-même : ce poste les affiche
454   * déjà, et les afficher deux fois ferait deux blocs pour une seule question —
455   * dont un qui ne tranche rien. */
456  window.accordsDistantsRelever = function () {
457    if (releveEnCours) return Promise.resolve(0)
458    releveEnCours = true
459    var moi = null
460    try { moi = (window.STATE && window.STATE.dispatchNodeId) || null } catch (e) { moi = null }
461    return api('GET', '/api/dispatch/accords').then(function (d) {
462      releveEnCours = false
463      var n = 0
464      ;((d && d.accords) || []).forEach(function (x) {
465        /* `publiees` NE SURVIT PAS À UN RECHARGEMENT, `nodeId` SI. Un poste qui
466           recharge sa page pendant qu'une de ses questions attend perdrait la
467           mémoire de l'avoir publiée, et se la reposerait à lui-même — un
468           second bloc pour une question dont il attend déjà la réponse. Le
469           registre, lui, sait de quel poste elle vient. */
470        if (moi && x && x.nodeId === moi) return
471        if (!x || !x.id || x.choix || publiees[x.id] || connues[x.id]) return
472        var reste = Number(x.expireA || 0) - Date.now()
473        if (reste < 5000) return
474        connues[x.id] = true
475        n++
476        /* LE POSTE PASSE TOUJOURS AVANT LE CONTEXTE, IL NE LE REMPLACE PAS.
477           Écrit d'abord `x.contexte || ('Demandé par…')`, ce qui faisait
478           disparaître le nom de la machine dès que la demande en portait un —
479           c'est-à-dire toujours, puisque la boucle de fond écrit « Tâche en
480           arrière-plan : … ». On autorisait alors une commande sans savoir
481           QUELLE machine allait l'exécuter. Trouvé par la garde. */
482        window.accordsPoser(x.nom, x.apercu || '',
483          '« ' + x.poste + ' » demande' + (x.contexte ? ' — ' + x.contexte : ''),
484          { distant: x.id, poste: x.poste, expireA: x.expireA })
485          .then(function (choix) {
486            /* UNE EXPIRATION ICI N'EST PAS UNE RÉPONSE. Le poste a sa propre
487               patience ; lui envoyer « refus » parce que CET écran a renoncé
488               trancherait à sa place, et à la place de l'appareil d'à côté qui
489               regardait peut-être la même question. */
490            if (choix === 'expire') return
491            repondreAuServeur(x.id, choix)
492          })
493      })
494      return n
495    }, function () { releveEnCours = false; return 0 })
496  }
497
498  /* ── LE RELEVÉ SE DÉMARRE LUI-MÊME, ET VOICI POURQUOI ────────────────────
499   *
500   * MESURÉ, PAS RELU. Il était d'abord lancé depuis l'organe Dispatch — le bon
501   * poste sur le papier. Sauf que `<script src="src/crea-dispatch.js">` vient
502   * AVANT `src/crea-accords.js` dans index.html comme dans client.html : au
503   * moment où cette ligne s'exécutait, `window.accordsDistantsSuivre` n'existait
504   * pas encore. Le `typeof` en faisait un silence parfait — la fonctionnalité
505   * entière ne démarrait jamais, et rien ne l'aurait dit.
506   *
507   * Un module qui dépend de l'ordre des balises est un module qu'on casse en
508   * déplaçant une ligne de HTML. Celui-ci s'allume tout seul.
509   *
510   * ET IL SE TAIT TANT QUE PERSONNE N'EST CONNECTÉ. `sessionPresumee` vit dans
511   * le tronc, chargé APRÈS ce fichier : on ne peut donc pas s'en servir comme
512   * condition de démarrage. On la relit à CHAQUE tour, ce qui règle les deux
513   * problèmes d'un coup — l'ordre des balises, et une page déconnectée qui
514   * appellerait une route protégée toutes les huit secondes jusqu'à la fin des
515   * temps. */
516  var releve = null
517  var PAS_RELEVE_MS = 8000
518  function releverSiQuelquUnPeutRepondre() {
519    var ouvert = true
520    try { ouvert = typeof window.sessionPresumee !== 'function' || !!window.sessionPresumee() }
521    catch (e) { ouvert = false }
522    if (!ouvert) return
523    window.accordsDistantsRelever()
524  }
525  window.accordsDistantsSuivre = function (actif) {
526    if (actif && !releve) {
527      releverSiQuelquUnPeutRepondre()
528      releve = setInterval(releverSiQuelquUnPeutRepondre, PAS_RELEVE_MS)
529    } else if (!actif && releve) { clearInterval(releve); releve = null }
530    return !!releve
531  }
532
533  /* SUR TOUS LES APPAREILS, y compris ceux qui exécutent : c'est justement le
534     téléphone, qui n'exécute rien, qui doit pouvoir répondre — et un poste
535     devant lequel quelqu'un se trouve peut répondre pour un autre. */
536  if (typeof setInterval === 'function') window.accordsDistantsSuivre(true)
537})()

Line numbers count LF bytes from the start of the resource, as the search results do. Vendor segments are library code the classifier recognised; they are stored but not indexed. Bytes are shown as Latin1 characters, one per byte.