@imenam/mcp-messenger 1.0.24 → 1.0.25

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/README.md CHANGED
@@ -424,7 +424,9 @@ correspondants et l'état de lecture :
424
424
  pastille, qui écrit à qui, heure — et sur **l'objet du message**, qui est ce qui
425
425
  l'identifie. Les deux forment le bandeau qui l'ouvre et le referme, cliquable d'un bout à
426
426
  l'autre et grisé au survol pour se donner à voir comme une cible. Ouvrir un message ne
427
- change rien à ce bandeau : le corps vient simplement se poser dessous.
427
+ change rien à ce bandeau : le corps vient simplement se poser dessous. Le destinataire
428
+ n'y figure pas — un fil ne lie que deux boîtes, le répéter à chaque ligne n'apprend
429
+ rien ;
428
430
  Un fil se parcourt avant de se lire, et le rafraîchissement automatique ne referme pas
429
431
  ce qui est en train d'être lu. **Le corps d'un message est rendu comme du markdown** : titres, listes,
430
432
  citations, tableaux, blocs de code, emphase et liens. C'est la langue dans laquelle les
@@ -434,9 +436,10 @@ correspondants et l'état de lecture :
434
436
  suivis. Une image n'est jamais chargée, elle est montrée par son intitulé et son lien.
435
437
  Les aperçus, eux, sont débarrassés de leur syntaxe par le serveur : ils tiennent sur une
436
438
  ligne, où un dièse ou une paire d'astérisques ne serait que du bruit ;
437
- - **le volet de détail** d'un message, ouvert à la demande : identifiants du message et du
438
- fil, les deux noms et l'adresse de chaque boîte, et le tableau de distribution (rôle,
439
- dossier, état). Tout ce qui se recopie a son bouton de copie ;
439
+ - **les détails d'un message**, dans une modale ouverte par l'icône d'information au bout
440
+ de sa ligne : l'objet, le nom et l'identifiant du fil, les identifiants, la date, la
441
+ priorité, les deux noms et l'adresse de chaque boîte, et le tableau de distribution
442
+ (rôle, dossier, état). Tout ce qui se recopie a son bouton de copie ;
440
443
  - une recherche unique, qui couvre le nom d'un fil, sa description et le texte de ses
441
444
  messages ;
442
445
  - **des panneaux redimensionnables** : chaque ligne de séparation se saisit à la souris,
@@ -447,8 +450,8 @@ correspondants et l'état de lecture :
447
450
 
448
451
  **Les adresses ne sont pas de l'information de lecture.** La console résout partout
449
452
  `box_9f2c4a01b3d7e5f8` en nom d'agent, et ne montre l'adresse qu'aux endroits où elle sert
450
- vraiment : le volet de détail, l'en-tête d'un fil et les boîtes sans nom, chaque fois avec
451
- un bouton de copie. Quand plusieurs sessions d'un même projet apparaissent, le nom de
453
+ vraiment : les détails d'un message, l'en-tête d'un fil et les boîtes sans nom, chaque fois
454
+ avec un bouton de copie. Quand plusieurs sessions d'un même projet apparaissent, le nom de
452
455
  l'agent est suivi de celui de la boîte entre parenthèses — sans quoi un fil se lirait
453
456
  « Documentor Dev → Documentor Dev » sans dire de quelles sessions il parle. Une boîte jamais connectée se distingue à son avatar en contour et à la mention
454
457
  « jamais connectée » — elle reste listée, puisqu'elle a du courrier en attente.
@@ -638,7 +641,7 @@ test/ # store, config, API HTTP, outils MCP, binaire réel
638
641
 
639
642
  ### Tests
640
643
 
641
- 284 tests couvrant : les suppressions de boîtes commandées depuis la console — une boîte
644
+ 287 tests couvrant : les suppressions de boîtes commandées depuis la console — une boîte
642
645
  désignée à la main, les vides, celles sans activité depuis une date — avec leur aperçu,
643
646
  leur double confirmation et le refus nominatif d'une boîte qui ne répond plus au critère, la reprise de la boîte d'une session interrompue (clé de session de
644
647
  l'hôte, boîte orpheline, boîte d'une session vivante jamais reprise), le rendu markdown des messages et l'aplatissement des aperçus (dont
@@ -26,6 +26,7 @@ const ICONS = {
26
26
  close: ["M18 6 6 18", "m6 6 12 12"],
27
27
  link: ["M10 13a5 5 0 0 0 7 0l3-3a5 5 0 0 0-7-7l-1 1", "M14 11a5 5 0 0 0-7 0l-3 3a5 5 0 0 0 7 7l1-1"],
28
28
  chevron: ["m9 18 6-6-6-6"],
29
+ info: ["M12 21a9 9 0 1 0 0-18 9 9 0 0 0 0 18z", "M12 11v5", "M12 8h.01"],
29
30
  trash: ["M3 6h18", "M8 6V4a1 1 0 0 1 1-1h6a1 1 0 0 1 1 1v2", "M19 6l-1 14a2 2 0 0 1-2 2H8a2 2 0 0 1-2-2L5 6"],
30
31
  };
31
32
 
@@ -52,8 +53,6 @@ const state = {
52
53
  filter: "all",
53
54
  search: "",
54
55
  threadId: null,
55
- /** Message dont le volet de détail est ouvert, ou null si le volet est fermé. */
56
- messageId: null,
57
56
  /**
58
57
  * Messages dépliés dans le lecteur, par identifiant. Vide au départ : un fil s'ouvre
59
58
  * replié, et se parcourt avant de se lire.
@@ -95,7 +94,8 @@ const el = {
95
94
  chips: document.getElementById("chips"),
96
95
  threads: document.getElementById("threads"),
97
96
  reader: document.getElementById("reader"),
98
- sheet: document.getElementById("sheet"),
97
+ details: document.getElementById("details"),
98
+ detailsBody: document.getElementById("details-body"),
99
99
  purge: document.getElementById("purge"),
100
100
  purgeOpen: document.getElementById("purge-open"),
101
101
  purgeClose: document.getElementById("purge-close"),
@@ -462,9 +462,6 @@ function select(kind, key) {
462
462
  if (isSelected(kind, key)) return;
463
463
  state.selection = { kind, key };
464
464
  state.threadId = null;
465
- state.messageId = null;
466
- el.sheet.hidden = true;
467
- el.console.classList.remove("has-sheet");
468
465
  refresh();
469
466
  }
470
467
 
@@ -900,8 +897,7 @@ function fillMessage(item, message, isLast) {
900
897
  const open = state.openMessages.has(message.message_id);
901
898
  const classes = ["message"];
902
899
  if (open) classes.push("is-expanded");
903
- if (state.messageId === message.message_id) classes.push("is-open");
904
- else if (isLast) classes.push("is-last");
900
+ if (isLast) classes.push("is-last");
905
901
  const row = node("div", classes.join(" "));
906
902
  const column = node("div", "message-body-col");
907
903
 
@@ -917,7 +913,6 @@ function fillMessage(item, message, isLast) {
917
913
 
918
914
  const identity = node("span", "message-identity");
919
915
  identity.appendChild(node("span", "message-from", party(message.from).name));
920
- identity.appendChild(node("span", "message-to", `→ ${party(message.to).name}`));
921
916
  if (message.priority === "high") identity.appendChild(tag("tag tag--high", "priorité haute"));
922
917
  if (message.priority === "low") identity.appendChild(tag("tag", "priorité basse"));
923
918
  if (!message.read) {
@@ -926,6 +921,7 @@ function fillMessage(item, message, isLast) {
926
921
  identity.appendChild(dot);
927
922
  }
928
923
  identity.appendChild(node("span", "message-time", formatShort(message.created_at)));
924
+ identity.appendChild(detailsButton(message));
929
925
  head.appendChild(identity);
930
926
 
931
927
  // L'objet, toujours : c'est le titre du message, et c'est lui qu'on cherche du regard.
@@ -957,14 +953,6 @@ function fillMessage(item, message, isLast) {
957
953
  ? tag("tag tag--ok", `lu par ${party(message.to).name}`, "check")
958
954
  : tag("tag tag--warn", `non lu par ${party(message.to).name}`, "clock")
959
955
  );
960
- // Le volet de détail, lui, s'ouvre à côté : c'est un autre état que le dépli du corps.
961
- const sheetOpen = state.messageId === message.message_id;
962
- foot.appendChild(
963
- button("btn btn--link", sheetOpen ? "détails ouverts" : "détails", () => {
964
- if (sheetOpen) closeSheet();
965
- else openMessage(message.message_id);
966
- })
967
- );
968
956
  content.appendChild(foot);
969
957
 
970
958
  row.appendChild(column);
@@ -983,44 +971,82 @@ function messageText(message) {
983
971
  return container;
984
972
  }
985
973
 
986
- // --- Volet de détail -------------------------------------------------------
974
+ // --- Détails d'un message --------------------------------------------------
975
+
976
+ /**
977
+ * Bouton d'information, au bout de la ligne.
978
+ *
979
+ * Un fil ne lie que deux boîtes : répéter à chaque message qui écrit à qui, sous quel nom
980
+ * de boîte, encombre la lecture pour une information déjà connue. Elle est donc rangée
981
+ * ici, à un clic, avec tout le reste — adresses, identifiants, distribution.
982
+ */
983
+ function detailsButton(message) {
984
+ const open = node("button", "message-info");
985
+ open.type = "button";
986
+ open.title = "Détails du message";
987
+ open.setAttribute("aria-label", "Détails du message");
988
+ open.appendChild(icon("info"));
989
+ // Le bandeau entier ouvre le message : ce clic-ci ne doit pas le replier au passage.
990
+ open.addEventListener("click", (event) => {
991
+ event.stopPropagation();
992
+ openDetails(message.message_id);
993
+ });
994
+ return open;
995
+ }
996
+
997
+ async function openDetails(messageId) {
998
+ el.detailsBody.replaceChildren(node("p", "muted", "Chargement…"));
999
+ el.details.showModal();
1000
+ try {
1001
+ renderDetails(await api(`/admin/messages/${encodeURIComponent(messageId)}`));
1002
+ } catch (error) {
1003
+ if (error.message === "unauthorized") el.details.close();
1004
+ else el.detailsBody.replaceChildren(node("p", "purge-error", `Erreur : ${error.message}`));
1005
+ }
1006
+ }
987
1007
 
988
- function renderSheet(message) {
1008
+ /** Tout ce que la conversation ne montre pas, et que la supervision peut vouloir lire. */
1009
+ function renderDetails(message) {
989
1010
  const container = document.createDocumentFragment();
990
1011
 
991
- const head = node("div", "sheet-head");
1012
+ const head = node("div", "modal-head");
992
1013
  const titles = node("div");
993
- titles.appendChild(node("h3", null, "Détails du message"));
994
- titles.appendChild(node("p", "sheet-subject", message.subject));
1014
+ titles.appendChild(node("h2", null, "Détails du message"));
1015
+ titles.appendChild(node("p", "details-subject", message.subject));
995
1016
  head.appendChild(titles);
996
1017
  const close = node("button", "btn btn--icon btn--ghost");
997
1018
  close.type = "button";
998
1019
  close.title = "Fermer";
999
- close.setAttribute("aria-label", "Fermer le volet");
1020
+ close.setAttribute("aria-label", "Fermer les détails");
1000
1021
  close.appendChild(icon("close"));
1001
- close.addEventListener("click", closeSheet);
1022
+ close.addEventListener("click", () => el.details.close());
1002
1023
  head.appendChild(close);
1003
1024
  container.appendChild(head);
1004
1025
 
1005
- const ids = node("div", "sheet-section");
1026
+ const ids = node("div", "details-section");
1006
1027
  ids.appendChild(node("span", "section-label", "Message"));
1007
1028
  ids.appendChild(kvCopy("Identifiant", message.message_id));
1008
- ids.appendChild(kvCopy("Fil", message.thread_id));
1029
+ ids.appendChild(kv("Objet", node("span", "kv-value", message.subject)));
1009
1030
  ids.appendChild(kv("Envoyé le", node("span", "kv-value", formatDate(message.created_at))));
1010
1031
  ids.appendChild(kv("Priorité", priorityTag(message.priority)));
1011
1032
  container.appendChild(ids);
1012
1033
 
1034
+ const thread = node("div", "details-section");
1035
+ thread.appendChild(node("span", "section-label", "Fil"));
1036
+ // Sans nom donné par un participant, le fil n'a que son identifiant et le sujet d'ouverture.
1037
+ thread.appendChild(kv("Nom", node("span", "kv-value", message.thread_title || "fil sans nom")));
1038
+ thread.appendChild(kvCopy("Identifiant", message.thread_id));
1039
+ container.appendChild(thread);
1040
+
1013
1041
  container.appendChild(partySection("De", message.from));
1014
1042
  container.appendChild(partySection("À", message.to));
1015
1043
 
1016
- const deliveries = node("div", "sheet-section");
1044
+ const deliveries = node("div", "details-section");
1017
1045
  deliveries.appendChild(node("span", "section-label", "Distribution"));
1018
1046
  deliveries.appendChild(deliveriesTable(message.deliveries || []));
1019
1047
  container.appendChild(deliveries);
1020
1048
 
1021
- el.sheet.replaceChildren(container);
1022
- el.sheet.hidden = false;
1023
- el.console.classList.add("has-sheet");
1049
+ el.detailsBody.replaceChildren(container);
1024
1050
  }
1025
1051
 
1026
1052
  function kv(key, valueNode) {
@@ -1050,7 +1076,7 @@ function priorityTag(priority) {
1050
1076
  */
1051
1077
  function partySection(label, agentId) {
1052
1078
  const who = party(agentId);
1053
- const section = node("div", "sheet-section");
1079
+ const section = node("div", "details-section");
1054
1080
  section.appendChild(node("span", "section-label", label));
1055
1081
 
1056
1082
  const block = node("div", "party");
@@ -1092,24 +1118,13 @@ function deliveriesTable(deliveries) {
1092
1118
  return table;
1093
1119
  }
1094
1120
 
1095
- function closeSheet() {
1096
- state.messageId = null;
1097
- el.sheet.hidden = true;
1098
- el.sheet.replaceChildren();
1099
- el.console.classList.remove("has-sheet");
1100
- if (state.threadId) refreshThread();
1101
- }
1102
-
1103
1121
  // --- Navigation ------------------------------------------------------------
1104
1122
 
1105
1123
  async function openThread(threadId) {
1106
1124
  state.threadId = threadId;
1107
- state.messageId = null;
1108
1125
  // Un fil s'ouvre replié, même celui qu'on rouvre : ce qui était déplié l'était pour
1109
1126
  // l'autre conversation.
1110
1127
  state.openMessages.clear();
1111
- el.sheet.hidden = true;
1112
- el.console.classList.remove("has-sheet");
1113
1128
  await refreshThread();
1114
1129
  await refresh({ keepThread: true });
1115
1130
  }
@@ -1126,16 +1141,6 @@ async function refreshThread() {
1126
1141
  }
1127
1142
  }
1128
1143
 
1129
- async function openMessage(messageId) {
1130
- state.messageId = messageId;
1131
- try {
1132
- renderSheet(await api(`/admin/messages/${encodeURIComponent(messageId)}`));
1133
- await refreshThread();
1134
- } catch (error) {
1135
- if (error.message !== "unauthorized") el.sheet.replaceChildren(node("div", "banner", error.message));
1136
- }
1137
- }
1138
-
1139
1144
  // --- Boucle de rafraîchissement --------------------------------------------
1140
1145
 
1141
1146
  async function refresh(options) {
@@ -1173,14 +1178,13 @@ async function refresh(options) {
1173
1178
  /**
1174
1179
  * Ce que chaque séparateur commande.
1175
1180
  *
1176
- * `edge` dit de quel côté se trouve le panneau réglé : les deux premiers bordent le
1177
- * panneau qui les précède, celui du volet de détail borde le panneau qui le suit. Sans
1178
- * cette distinction, tirer vers la droite élargirait un volet ancré à droite.
1181
+ * `edge` dit de quel côté se trouve le panneau réglé : ici, chacun borde le panneau qui
1182
+ * le précède. La distinction reste écrite car un panneau ancré à droite se réglerait à
1183
+ * l'envers, en comptant depuis son bord droit.
1179
1184
  */
1180
1185
  const PANELS = {
1181
1186
  "resize-agents": { name: "--w-agents", panel: "agentsPane", edge: "before", min: 180, max: 520, initial: 240 },
1182
1187
  "resize-threads": { name: "--w-threads", panel: "threadsPane", edge: "before", min: 260, max: 720, initial: 400 },
1183
- "resize-sheet": { name: "--w-sheet", panel: "sheet", edge: "after", min: 280, max: 720, initial: 360 },
1184
1188
  };
1185
1189
 
1186
1190
  const LAYOUT_KEY = "messenger.layout";
@@ -1457,9 +1461,6 @@ async function confirmPurge() {
1457
1461
  state.selection = { kind: "all", key: null };
1458
1462
  }
1459
1463
  state.threadId = null;
1460
- state.messageId = null;
1461
- el.sheet.hidden = true;
1462
- el.console.classList.remove("has-sheet");
1463
1464
  await refresh();
1464
1465
  } catch (error) {
1465
1466
  purge.armed = false;
@@ -1547,11 +1548,9 @@ el.logout.addEventListener("click", async () => {
1547
1548
  state.expanded = new Set();
1548
1549
  state.showEmpty = new Set();
1549
1550
  state.threadId = null;
1550
- state.messageId = null;
1551
+ state.openMessages = new Set();
1551
1552
  state.directory = new Map();
1552
1553
  state.groups = [];
1553
- el.sheet.hidden = true;
1554
- el.console.classList.remove("has-sheet");
1555
1554
  el.agents.replaceChildren();
1556
1555
  el.threads.replaceChildren();
1557
1556
  renderEmptyReader();
@@ -1,217 +1,215 @@
1
- <!doctype html>
2
- <html lang="fr">
3
- <head>
4
- <meta charset="utf-8" />
5
- <meta name="viewport" content="width=device-width, initial-scale=1" />
6
- <title>MCP Messenger — supervision</title>
7
- <link rel="stylesheet" href="./styles.css" />
8
- </head>
9
- <body>
10
- <header class="topbar">
11
- <div class="brand">
12
- <svg class="brand-mark" viewBox="0 0 24 24" aria-hidden="true">
13
- <path d="M22 12h-6l-2 3h-4l-2-3H2" />
14
- <path d="M5.45 5.11 2 12v6a2 2 0 0 0 2 2h16a2 2 0 0 0 2-2v-6l-3.45-6.89A2 2 0 0 0 16.76 4H7.24a2 2 0 0 0-1.79 1.11z" />
15
- </svg>
16
- <h1>MCP Messenger</h1>
17
- <span class="pill pill--idle" id="status">
18
- <span class="pill-dot"></span>
19
- <span id="status-label">connexion…</span>
20
- </span>
21
- </div>
22
-
23
- <div class="topbar-search" id="search-box">
24
- <svg class="icon" viewBox="0 0 24 24" aria-hidden="true">
25
- <circle cx="11" cy="11" r="8" />
26
- <path d="m21 21-4.3-4.3" />
27
- </svg>
28
- <input type="search" id="search" placeholder="Rechercher un sujet, un fil, un agent…" />
29
- </div>
30
-
31
- <div class="topbar-controls" id="controls" hidden>
32
- <label class="switch">
33
- <input type="checkbox" id="auto-refresh" checked />
34
- <span class="switch-track"><span class="switch-knob"></span></span>
35
- <span class="switch-label">auto · 5 s</span>
36
- </label>
37
- <button type="button" id="refresh" class="btn btn--icon" title="Rafraîchir" aria-label="Rafraîchir">
38
- <svg class="icon" viewBox="0 0 24 24" aria-hidden="true">
39
- <path d="M3 12a9 9 0 0 1 15-6.7L21 8" />
40
- <path d="M21 3v5h-5" />
41
- <path d="M21 12a9 9 0 0 1-15 6.7L3 16" />
42
- <path d="M3 21v-5h5" />
43
- </svg>
44
- </button>
45
- <button type="button" id="purge-open" class="btn btn--icon" title="Purger les boîtes inactives" aria-label="Purger les boîtes inactives">
46
- <svg class="icon" viewBox="0 0 24 24" aria-hidden="true">
47
- <path d="M3 6h18" />
48
- <path d="M8 6V4a1 1 0 0 1 1-1h6a1 1 0 0 1 1 1v2" />
49
- <path d="M19 6l-1 14a2 2 0 0 1-2 2H8a2 2 0 0 1-2-2L5 6" />
50
- </svg>
51
- </button>
52
- <button type="button" id="logout" class="btn btn--link">Se déconnecter</button>
53
- </div>
54
- </header>
55
-
56
- <!--
57
- Purge des boîtes inactives : la seule action de la console qui écrit, et elle
58
- détruit. Elle se fait donc en deux temps, l'aperçu puis la confirmation, et ne porte
59
- jamais que sur les boîtes affichées.
60
- -->
61
- <dialog class="modal" id="purge">
62
- <div class="modal-card">
63
- <div class="modal-head">
64
- <h2>Nettoyer les boîtes</h2>
65
- <button type="button" class="btn btn--icon btn--ghost" id="purge-close" aria-label="Fermer">
66
- <svg class="icon" viewBox="0 0 24 24" aria-hidden="true">
67
- <path d="M18 6 6 18" />
68
- <path d="m6 6 12 12" />
69
- </svg>
70
- </button>
71
- </div>
72
- <p class="modal-sub">
73
- Deux nettoyages, deux critères. La suppression est définitive dans les deux cas, et emporte les messages que
74
- plus personne ne pourrait lire ; ce que le correspondant a reçu de son côté reste intact. Un agent dont toutes
75
- les boîtes disparaissent quitte l'annuaire avec elles.
76
- </p>
77
-
78
- <!-- Premier critère : la boîte n'a jamais servi. Il ne dépend d'aucune date. -->
79
- <div class="purge-controls">
80
- <div class="purge-criterion">
81
- <span class="field-label">Boîtes vides</span>
82
- <span class="purge-hint">Rien n'y est jamais passé, ni envoi ni réception, quel que soit leur âge.</span>
83
- </div>
84
- <button type="button" class="btn" id="purge-preview-empty">Prévisualiser</button>
85
- </div>
86
-
87
- <!-- Second critère : la boîte a servi, mais plus rien ne l'a touchée depuis. -->
88
- <div class="purge-controls">
89
- <label class="field">
90
- <span class="field-label">Boîtes sans activité depuis le</span>
91
- <span class="field-input"><input type="date" id="purge-before" /></span>
92
- <span class="purge-hint">Ni connexion, ni message. Une boîte créée après cette date est épargnée.</span>
93
- </label>
94
- <button type="button" class="btn" id="purge-preview">Prévisualiser</button>
95
- </div>
96
-
97
- <div class="purge-result" id="purge-result"></div>
98
-
99
- <div class="modal-foot">
100
- <button type="button" class="btn btn--link" id="purge-cancel">Fermer</button>
101
- <button type="button" class="btn btn--danger" id="purge-confirm" disabled>Supprimer</button>
102
- </div>
103
- </div>
104
- </dialog>
105
-
106
- <!-- Écran de connexion : une carte seule, sans les panneaux vides derrière elle. -->
107
- <main class="login" id="login" hidden>
108
- <form class="card" id="login-form">
109
- <div class="card-badge">
110
- <svg class="icon" viewBox="0 0 24 24" aria-hidden="true">
111
- <rect x="3" y="11" width="18" height="11" rx="2" />
112
- <path d="M7 11V7a5 5 0 0 1 10 0v4" />
113
- </svg>
114
- </div>
115
- <h2>Console de supervision</h2>
116
- <p class="card-sub" id="login-origin">MCP Messenger</p>
117
-
118
- <label class="field">
119
- <span class="field-label">Jeton administrateur</span>
120
- <span class="field-input">
121
- <input type="password" id="token" autocomplete="current-password" placeholder="MESSENGER_ADMIN_TOKEN" />
122
- <button type="button" class="field-eye" id="token-reveal" aria-label="Afficher le jeton">
123
- <svg class="icon" viewBox="0 0 24 24" aria-hidden="true">
124
- <path d="M2 12s3.6-7 10-7 10 7 10 7-3.6 7-10 7-10-7-10-7z" />
125
- <circle cx="12" cy="12" r="3" />
126
- </svg>
127
- </button>
128
- </span>
129
- <span class="field-error" id="token-error"></span>
130
- </label>
131
-
132
- <button type="submit" class="btn btn--primary btn--block">Se connecter</button>
133
- <p class="card-note">
134
- Le jeton n'est jamais conservé dans le navigateur : la session tient dans un cookie HttpOnly, fermé à la
135
- déconnexion.
136
- </p>
137
- </form>
138
- </main>
139
-
140
- <!-- Console : agents, fils, lecteur du fil, volet de détail. -->
141
- <main class="console" id="console" hidden>
142
- <aside class="agents" id="agents-pane">
143
- <div class="agents-head">
144
- <span class="section-label">Agents</span>
145
- <button type="button" class="btn btn--icon btn--ghost" id="collapse" title="Replier" aria-label="Replier la liste des agents" aria-expanded="true">
146
- <svg class="icon" viewBox="0 0 24 24" aria-hidden="true">
147
- <rect x="3" y="3" width="18" height="18" rx="2" />
148
- <path d="M9 3v18" />
149
- </svg>
150
- </button>
151
- </div>
152
- <ul class="agent-list" id="agents"></ul>
153
- <p class="agents-foot muted" id="agents-foot"></p>
154
- </aside>
155
-
156
- <!--
157
- Séparateurs déplaçables. Chacun redimensionne le panneau qu'il borde, à la souris
158
- comme aux flèches du clavier ; un double-clic rend sa largeur d'origine.
159
- -->
160
- <div
161
- class="resizer"
162
- id="resize-agents"
163
- role="separator"
164
- aria-orientation="vertical"
165
- aria-label="Largeur de la colonne des agents"
166
- tabindex="0"
167
- ></div>
168
-
169
- <section class="threads">
170
- <div class="threads-head">
171
- <!--
172
- L'en-tête dit sur quoi la liste est filtrée. Quand c'est un agent, il y tient la
173
- place principale, pastille comprise : c'est le sujet de ce qu'on lit en dessous.
174
- -->
175
- <div class="threads-title">
176
- <span class="threads-avatar" id="threads-avatar" hidden></span>
177
- <div class="threads-heading">
178
- <h2 id="threads-name">Fils</h2>
179
- <p class="muted" id="threads-scope"></p>
180
- </div>
181
- </div>
182
- <div class="chips" id="chips">
183
- <button type="button" class="chip is-active" data-filter="all">Tous</button>
184
- <button type="button" class="chip" data-filter="unread">Non lus</button>
185
- <button type="button" class="chip" data-filter="high">Priorité haute</button>
186
- </div>
187
- </div>
188
- <ul class="thread-list" id="threads"></ul>
189
- </section>
190
-
191
- <div
192
- class="resizer"
193
- id="resize-threads"
194
- role="separator"
195
- aria-orientation="vertical"
196
- aria-label="Largeur de la liste des fils"
197
- tabindex="0"
198
- ></div>
199
-
200
- <section class="reader" id="reader"></section>
201
-
202
- <!-- Suit le volet de détail : il n'existe que lorsque le volet est ouvert. -->
203
- <div
204
- class="resizer resizer--sheet"
205
- id="resize-sheet"
206
- role="separator"
207
- aria-orientation="vertical"
208
- aria-label="Largeur du volet de détail"
209
- tabindex="0"
210
- ></div>
211
-
212
- <aside class="sheet" id="sheet" hidden></aside>
213
- </main>
214
-
215
- <script type="module" src="./app.js"></script>
216
- </body>
217
- </html>
1
+ <!doctype html>
2
+ <html lang="fr">
3
+ <head>
4
+ <meta charset="utf-8" />
5
+ <meta name="viewport" content="width=device-width, initial-scale=1" />
6
+ <title>MCP Messenger — supervision</title>
7
+ <link rel="stylesheet" href="./styles.css" />
8
+ </head>
9
+ <body>
10
+ <header class="topbar">
11
+ <div class="brand">
12
+ <svg class="brand-mark" viewBox="0 0 24 24" aria-hidden="true">
13
+ <path d="M22 12h-6l-2 3h-4l-2-3H2" />
14
+ <path d="M5.45 5.11 2 12v6a2 2 0 0 0 2 2h16a2 2 0 0 0 2-2v-6l-3.45-6.89A2 2 0 0 0 16.76 4H7.24a2 2 0 0 0-1.79 1.11z" />
15
+ </svg>
16
+ <h1>MCP Messenger</h1>
17
+ <span class="pill pill--idle" id="status">
18
+ <span class="pill-dot"></span>
19
+ <span id="status-label">connexion…</span>
20
+ </span>
21
+ </div>
22
+
23
+ <div class="topbar-search" id="search-box">
24
+ <svg class="icon" viewBox="0 0 24 24" aria-hidden="true">
25
+ <circle cx="11" cy="11" r="8" />
26
+ <path d="m21 21-4.3-4.3" />
27
+ </svg>
28
+ <input type="search" id="search" placeholder="Rechercher un sujet, un fil, un agent…" />
29
+ </div>
30
+
31
+ <div class="topbar-controls" id="controls" hidden>
32
+ <label class="switch">
33
+ <input type="checkbox" id="auto-refresh" checked />
34
+ <span class="switch-track"><span class="switch-knob"></span></span>
35
+ <span class="switch-label">auto · 5 s</span>
36
+ </label>
37
+ <button type="button" id="refresh" class="btn btn--icon" title="Rafraîchir" aria-label="Rafraîchir">
38
+ <svg class="icon" viewBox="0 0 24 24" aria-hidden="true">
39
+ <path d="M3 12a9 9 0 0 1 15-6.7L21 8" />
40
+ <path d="M21 3v5h-5" />
41
+ <path d="M21 12a9 9 0 0 1-15 6.7L3 16" />
42
+ <path d="M3 21v-5h5" />
43
+ </svg>
44
+ </button>
45
+ <button type="button" id="purge-open" class="btn btn--icon" title="Purger les boîtes inactives" aria-label="Purger les boîtes inactives">
46
+ <svg class="icon" viewBox="0 0 24 24" aria-hidden="true">
47
+ <path d="M3 6h18" />
48
+ <path d="M8 6V4a1 1 0 0 1 1-1h6a1 1 0 0 1 1 1v2" />
49
+ <path d="M19 6l-1 14a2 2 0 0 1-2 2H8a2 2 0 0 1-2-2L5 6" />
50
+ </svg>
51
+ </button>
52
+ <button type="button" id="logout" class="btn btn--link">Se déconnecter</button>
53
+ </div>
54
+ </header>
55
+
56
+ <!--
57
+ Purge des boîtes inactives : la seule action de la console qui écrit, et elle
58
+ détruit. Elle se fait donc en deux temps, l'aperçu puis la confirmation, et ne porte
59
+ jamais que sur les boîtes affichées.
60
+ -->
61
+ <dialog class="modal" id="purge">
62
+ <div class="modal-card">
63
+ <div class="modal-head">
64
+ <h2>Nettoyer les boîtes</h2>
65
+ <button type="button" class="btn btn--icon btn--ghost" id="purge-close" aria-label="Fermer">
66
+ <svg class="icon" viewBox="0 0 24 24" aria-hidden="true">
67
+ <path d="M18 6 6 18" />
68
+ <path d="m6 6 12 12" />
69
+ </svg>
70
+ </button>
71
+ </div>
72
+ <p class="modal-sub">
73
+ Deux nettoyages, deux critères. La suppression est définitive dans les deux cas, et emporte les messages que
74
+ plus personne ne pourrait lire ; ce que le correspondant a reçu de son côté reste intact. Un agent dont toutes
75
+ les boîtes disparaissent quitte l'annuaire avec elles.
76
+ </p>
77
+
78
+ <!-- Premier critère : la boîte n'a jamais servi. Il ne dépend d'aucune date. -->
79
+ <div class="purge-controls">
80
+ <div class="purge-criterion">
81
+ <span class="field-label">Boîtes vides</span>
82
+ <span class="purge-hint">Rien n'y est jamais passé, ni envoi ni réception, quel que soit leur âge.</span>
83
+ </div>
84
+ <button type="button" class="btn" id="purge-preview-empty">Prévisualiser</button>
85
+ </div>
86
+
87
+ <!-- Second critère : la boîte a servi, mais plus rien ne l'a touchée depuis. -->
88
+ <div class="purge-controls">
89
+ <label class="field">
90
+ <span class="field-label">Boîtes sans activité depuis le</span>
91
+ <span class="field-input"><input type="date" id="purge-before" /></span>
92
+ <span class="purge-hint">Ni connexion, ni message. Une boîte créée après cette date est épargnée.</span>
93
+ </label>
94
+ <button type="button" class="btn" id="purge-preview">Prévisualiser</button>
95
+ </div>
96
+
97
+ <div class="purge-result" id="purge-result"></div>
98
+
99
+ <div class="modal-foot">
100
+ <button type="button" class="btn btn--link" id="purge-cancel">Fermer</button>
101
+ <button type="button" class="btn btn--danger" id="purge-confirm" disabled>Supprimer</button>
102
+ </div>
103
+ </div>
104
+ </dialog>
105
+
106
+ <!--
107
+ Détails d'un message : tout ce que la conversation ne montre pas — adresses, noms de
108
+ boîtes, identifiants, distribution. Une modale, parce que c'est une consultation
109
+ ponctuelle : elle s'ouvre sur demande et rend la place en se fermant.
110
+ -->
111
+ <dialog class="modal" id="details">
112
+ <div class="modal-card" id="details-body"></div>
113
+ </dialog>
114
+
115
+ <!-- Écran de connexion : une carte seule, sans les panneaux vides derrière elle. -->
116
+ <main class="login" id="login" hidden>
117
+ <form class="card" id="login-form">
118
+ <div class="card-badge">
119
+ <svg class="icon" viewBox="0 0 24 24" aria-hidden="true">
120
+ <rect x="3" y="11" width="18" height="11" rx="2" />
121
+ <path d="M7 11V7a5 5 0 0 1 10 0v4" />
122
+ </svg>
123
+ </div>
124
+ <h2>Console de supervision</h2>
125
+ <p class="card-sub" id="login-origin">MCP Messenger</p>
126
+
127
+ <label class="field">
128
+ <span class="field-label">Jeton administrateur</span>
129
+ <span class="field-input">
130
+ <input type="password" id="token" autocomplete="current-password" placeholder="MESSENGER_ADMIN_TOKEN" />
131
+ <button type="button" class="field-eye" id="token-reveal" aria-label="Afficher le jeton">
132
+ <svg class="icon" viewBox="0 0 24 24" aria-hidden="true">
133
+ <path d="M2 12s3.6-7 10-7 10 7 10 7-3.6 7-10 7-10-7-10-7z" />
134
+ <circle cx="12" cy="12" r="3" />
135
+ </svg>
136
+ </button>
137
+ </span>
138
+ <span class="field-error" id="token-error"></span>
139
+ </label>
140
+
141
+ <button type="submit" class="btn btn--primary btn--block">Se connecter</button>
142
+ <p class="card-note">
143
+ Le jeton n'est jamais conservé dans le navigateur : la session tient dans un cookie HttpOnly, fermé à la
144
+ déconnexion.
145
+ </p>
146
+ </form>
147
+ </main>
148
+
149
+ <!-- Console : agents, fils, lecteur du fil. -->
150
+ <main class="console" id="console" hidden>
151
+ <aside class="agents" id="agents-pane">
152
+ <div class="agents-head">
153
+ <span class="section-label">Agents</span>
154
+ <button type="button" class="btn btn--icon btn--ghost" id="collapse" title="Replier" aria-label="Replier la liste des agents" aria-expanded="true">
155
+ <svg class="icon" viewBox="0 0 24 24" aria-hidden="true">
156
+ <rect x="3" y="3" width="18" height="18" rx="2" />
157
+ <path d="M9 3v18" />
158
+ </svg>
159
+ </button>
160
+ </div>
161
+ <ul class="agent-list" id="agents"></ul>
162
+ <p class="agents-foot muted" id="agents-foot"></p>
163
+ </aside>
164
+
165
+ <!--
166
+ Séparateurs déplaçables. Chacun redimensionne le panneau qu'il borde, à la souris
167
+ comme aux flèches du clavier ; un double-clic rend sa largeur d'origine.
168
+ -->
169
+ <div
170
+ class="resizer"
171
+ id="resize-agents"
172
+ role="separator"
173
+ aria-orientation="vertical"
174
+ aria-label="Largeur de la colonne des agents"
175
+ tabindex="0"
176
+ ></div>
177
+
178
+ <section class="threads">
179
+ <div class="threads-head">
180
+ <!--
181
+ L'en-tête dit sur quoi la liste est filtrée. Quand c'est un agent, il y tient la
182
+ place principale, pastille comprise : c'est le sujet de ce qu'on lit en dessous.
183
+ -->
184
+ <div class="threads-title">
185
+ <span class="threads-avatar" id="threads-avatar" hidden></span>
186
+ <div class="threads-heading">
187
+ <h2 id="threads-name">Fils</h2>
188
+ <p class="muted" id="threads-scope"></p>
189
+ </div>
190
+ </div>
191
+ <div class="chips" id="chips">
192
+ <button type="button" class="chip is-active" data-filter="all">Tous</button>
193
+ <button type="button" class="chip" data-filter="unread">Non lus</button>
194
+ <button type="button" class="chip" data-filter="high">Priorité haute</button>
195
+ </div>
196
+ </div>
197
+ <ul class="thread-list" id="threads"></ul>
198
+ </section>
199
+
200
+ <div
201
+ class="resizer"
202
+ id="resize-threads"
203
+ role="separator"
204
+ aria-orientation="vertical"
205
+ aria-label="Largeur de la liste des fils"
206
+ tabindex="0"
207
+ ></div>
208
+
209
+ <section class="reader" id="reader"></section>
210
+
211
+ </main>
212
+
213
+ <script type="module" src="./app.js"></script>
214
+ </body>
215
+ </html>
@@ -554,29 +554,16 @@ body {
554
554
  .console {
555
555
  --w-agents: 240px;
556
556
  --w-threads: 400px;
557
- --w-sheet: 360px;
558
557
  --w-gutter: 5px;
559
558
  display: grid;
560
559
  grid-template-columns: var(--w-agents) var(--w-gutter) var(--w-threads) var(--w-gutter) minmax(0, 1fr);
561
560
  height: calc(100vh - var(--topbar-h));
562
561
  }
563
562
 
564
- .console.has-sheet {
565
- grid-template-columns:
566
- var(--w-agents) var(--w-gutter) var(--w-threads) var(--w-gutter)
567
- minmax(0, 1fr) var(--w-gutter) var(--w-sheet);
568
- }
569
-
570
563
  .console.is-collapsed {
571
564
  grid-template-columns: 57px var(--w-gutter) var(--w-threads) var(--w-gutter) minmax(0, 1fr);
572
565
  }
573
566
 
574
- .console.is-collapsed.has-sheet {
575
- grid-template-columns:
576
- 57px var(--w-gutter) var(--w-threads) var(--w-gutter)
577
- minmax(0, 1fr) var(--w-gutter) var(--w-sheet);
578
- }
579
-
580
567
  /* --- Séparateurs déplaçables ---------------------------------------------- */
581
568
 
582
569
  .resizer {
@@ -598,11 +585,6 @@ body {
598
585
  outline: none;
599
586
  }
600
587
 
601
- /* Le volet de détail n'est pas toujours là ; son séparateur non plus. */
602
- .console:not(.has-sheet) .resizer--sheet {
603
- display: none;
604
- }
605
-
606
588
  /* Colonne repliée : il n'y a plus de largeur à régler, seulement un rail d'avatars. */
607
589
  .console.is-collapsed #resize-agents {
608
590
  cursor: default;
@@ -617,8 +599,7 @@ body {
617
599
 
618
600
  .console.is-resizing .agents,
619
601
  .console.is-resizing .threads,
620
- .console.is-resizing .reader,
621
- .console.is-resizing .sheet {
602
+ .console.is-resizing .reader {
622
603
  pointer-events: none;
623
604
  }
624
605
 
@@ -1420,6 +1401,30 @@ body {
1420
1401
  outline-offset: -2px;
1421
1402
  }
1422
1403
 
1404
+ /*
1405
+ L'information s'ouvre depuis la ligne sans la replier. Visuellement c'est un bouton posé
1406
+ dans un autre ; dans le balisage, non — il est rangé au bout de la ligne d'identité, et
1407
+ son clic ne remonte pas au bandeau.
1408
+ */
1409
+ .message-info {
1410
+ display: flex;
1411
+ padding: 2px;
1412
+ border: 0;
1413
+ border-radius: 50%;
1414
+ background: none;
1415
+ color: var(--muted);
1416
+ cursor: pointer;
1417
+ }
1418
+
1419
+ .message-info:hover {
1420
+ color: var(--accent);
1421
+ }
1422
+
1423
+ .message-info:focus-visible {
1424
+ outline: 2px solid var(--accent);
1425
+ outline-offset: 1px;
1426
+ }
1427
+
1423
1428
  .message-head > .avatar {
1424
1429
  grid-row: span 2;
1425
1430
  }
@@ -1435,11 +1440,11 @@ body {
1435
1440
 
1436
1441
  /*
1437
1442
  L'objet du message, sur sa ligne, replié comme déplié : c'est ce qui identifie le
1438
- message, et le bandeau ne doit pas changer de forme en s'ouvrant. Ce qui dépasse est
1439
- coupé — la ligne suivante appartient au message suivant.
1443
+ message, et le bandeau ne doit pas changer de forme en s'ouvrant. Il se lit sans graisse
1444
+ — c'est déjà la ligne la plus longue du bandeau, l'appuyer en ferait un titre de section.
1445
+ Ce qui dépasse est coupé : la ligne suivante appartient au message suivant.
1440
1446
  */
1441
1447
  .message-subject {
1442
- font-weight: 600;
1443
1448
  min-width: 0;
1444
1449
  white-space: nowrap;
1445
1450
  overflow: hidden;
@@ -1464,11 +1469,6 @@ body {
1464
1469
  font-weight: 600;
1465
1470
  }
1466
1471
 
1467
- .message-to {
1468
- font-size: 13px;
1469
- color: var(--muted);
1470
- }
1471
-
1472
1472
  .message-time {
1473
1473
  margin-left: auto;
1474
1474
  font-size: 12px;
@@ -1639,48 +1639,26 @@ body {
1639
1639
  font-size: 12px;
1640
1640
  }
1641
1641
 
1642
- /* --- Volet de détail ------------------------------------------------------ */
1642
+ /* --- Détails d'un message ------------------------------------------------- */
1643
1643
 
1644
- .sheet {
1645
- border-left: 1px solid var(--border);
1646
- background: var(--panel);
1647
- overflow-y: auto;
1648
- min-width: 0;
1649
- }
1650
-
1651
- .sheet-head {
1652
- display: flex;
1653
- align-items: flex-start;
1654
- gap: 8px;
1655
- padding: 18px 20px 14px;
1656
- border-bottom: 1px solid var(--border);
1657
- }
1658
-
1659
- .sheet-head h3 {
1660
- margin: 0;
1661
- font-size: 15px;
1662
- }
1663
-
1664
- .sheet-subject {
1644
+ /* L'objet, sous le titre de la modale : il dit de quel message on lit les détails. */
1645
+ .details-subject {
1665
1646
  margin: 4px 0 0;
1666
1647
  font-size: 13px;
1667
1648
  color: var(--muted);
1668
1649
  }
1669
1650
 
1670
- .sheet-head .btn {
1671
- margin-left: auto;
1672
- }
1673
-
1674
- .sheet-section {
1675
- padding: 16px 20px;
1676
- border-bottom: 1px solid var(--border);
1651
+ .details-section {
1652
+ padding: 16px 0;
1653
+ border-bottom: 1px solid var(--border-soft);
1677
1654
  }
1678
1655
 
1679
- .sheet-section:last-of-type {
1656
+ .details-section:last-of-type {
1680
1657
  border-bottom: 0;
1658
+ padding-bottom: 0;
1681
1659
  }
1682
1660
 
1683
- .sheet-section .section-label {
1661
+ .details-section .section-label {
1684
1662
  display: block;
1685
1663
  margin-bottom: 12px;
1686
1664
  }
@@ -1817,13 +1795,6 @@ body {
1817
1795
 
1818
1796
  /* --- Écrans étroits ------------------------------------------------------- */
1819
1797
 
1820
- @media (max-width: 1200px) {
1821
- .console.has-sheet,
1822
- .console.is-collapsed.has-sheet {
1823
- grid-template-columns: 57px 320px minmax(0, 1fr) 320px;
1824
- }
1825
- }
1826
-
1827
1798
  @media (max-width: 1000px) {
1828
1799
  .topbar-search {
1829
1800
  max-width: 240px;
@@ -1831,22 +1802,6 @@ body {
1831
1802
 
1832
1803
  .console,
1833
1804
  .console.is-collapsed {
1834
- grid-template-columns: 57px 300px minmax(0, 1fr);
1835
- }
1836
-
1837
- /* Trop étroit pour quatre colonnes : le volet recouvre le lecteur. */
1838
- .console.has-sheet,
1839
- .console.is-collapsed.has-sheet {
1840
- grid-template-columns: 57px 300px minmax(0, 1fr);
1841
- }
1842
-
1843
- .console.has-sheet .sheet {
1844
- position: fixed;
1845
- top: var(--topbar-h);
1846
- right: 0;
1847
- bottom: 0;
1848
- width: 340px;
1849
- max-width: 100%;
1850
- box-shadow: -8px 0 24px rgba(0, 0, 0, 0.12);
1805
+ grid-template-columns: 57px var(--w-gutter) 300px var(--w-gutter) minmax(0, 1fr);
1851
1806
  }
1852
1807
  }
@@ -1 +1 @@
1
- export declare const APP_VERSION = "1.0.24";
1
+ export declare const APP_VERSION = "1.0.25";
@@ -1,2 +1,2 @@
1
1
  // Généré par scripts/generate-version.js — ne pas modifier à la main.
2
- export const APP_VERSION = "1.0.24";
2
+ export const APP_VERSION = "1.0.25";
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@imenam/mcp-messenger",
3
- "version": "1.0.24",
3
+ "version": "1.0.25",
4
4
  "main": "dist/src/index.js",
5
5
  "scripts": {
6
6
  "test": "npm run build && vitest run",