@imenam/mcp-messenger 1.0.23 → 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 +19 -8
- package/dist/src/server/ui/app.js +136 -76
- package/dist/src/server/ui/index.html +215 -217
- package/dist/src/server/ui/styles.css +151 -96
- package/dist/src/version.d.ts +1 -1
- package/dist/src/version.js +1 -1
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -391,7 +391,9 @@ correspondants et l'état de lecture :
|
|
|
391
391
|
**replié par défaut**, et sélectionner un agent n'ouvre rien. Recliquer ne relâche rien
|
|
392
392
|
non plus : on revient à la vue générale par « Tous les agents », en tête de colonne. La
|
|
393
393
|
colonne se replie sur les seuls avatars d'agents quand la place manque, son bouton de
|
|
394
|
-
repli restant en place : c'est lui qui la rouvre.
|
|
394
|
+
repli restant en place : c'est lui qui la rouvre. **Repliée, elle garde deux signes
|
|
395
|
+
distincts** : l'anneau désigne l'agent sélectionné, la pastille bleue dit qu'il reste du
|
|
396
|
+
non-lu — la même convention que le point de la liste des fils.
|
|
395
397
|
|
|
396
398
|
**Le classement se fait en deux temps : d'abord ce qui a servi, ensuite ce qui est
|
|
397
399
|
vide ; puis, dans chacun des deux, la dernière activité, la plus récente en tête.** Une
|
|
@@ -418,7 +420,15 @@ correspondants et l'état de lecture :
|
|
|
418
420
|
- **trois puces** au-dessus de la liste : tous, non lus, priorité haute ;
|
|
419
421
|
- **le lecteur du fil** : la conversation entière dans l'ordre, un bloc par message, avec
|
|
420
422
|
qui écrit à qui — en clair, pas en adresses — et une pastille disant si le destinataire
|
|
421
|
-
l'a lu. **
|
|
423
|
+
l'a lu. **Les messages s'ouvrent repliés** : chacun tient alors sur sa ligne d'identité —
|
|
424
|
+
pastille, qui écrit à qui, heure — et sur **l'objet du message**, qui est ce qui
|
|
425
|
+
l'identifie. Les deux forment le bandeau qui l'ouvre et le referme, cliquable d'un bout à
|
|
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. 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 ;
|
|
430
|
+
Un fil se parcourt avant de se lire, et le rafraîchissement automatique ne referme pas
|
|
431
|
+
ce qui est en train d'être lu. **Le corps d'un message est rendu comme du markdown** : titres, listes,
|
|
422
432
|
citations, tableaux, blocs de code, emphase et liens. C'est la langue dans laquelle les
|
|
423
433
|
agents s'écrivent, la console la lit donc ainsi plutôt que d'afficher sa syntaxe. Le
|
|
424
434
|
rendu construit des nœuds un par un et n'insère jamais de HTML : rien de ce qu'un agent
|
|
@@ -426,9 +436,10 @@ correspondants et l'état de lecture :
|
|
|
426
436
|
suivis. Une image n'est jamais chargée, elle est montrée par son intitulé et son lien.
|
|
427
437
|
Les aperçus, eux, sont débarrassés de leur syntaxe par le serveur : ils tiennent sur une
|
|
428
438
|
ligne, où un dièse ou une paire d'astérisques ne serait que du bruit ;
|
|
429
|
-
- **
|
|
430
|
-
|
|
431
|
-
|
|
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 ;
|
|
432
443
|
- une recherche unique, qui couvre le nom d'un fil, sa description et le texte de ses
|
|
433
444
|
messages ;
|
|
434
445
|
- **des panneaux redimensionnables** : chaque ligne de séparation se saisit à la souris,
|
|
@@ -439,8 +450,8 @@ correspondants et l'état de lecture :
|
|
|
439
450
|
|
|
440
451
|
**Les adresses ne sont pas de l'information de lecture.** La console résout partout
|
|
441
452
|
`box_9f2c4a01b3d7e5f8` en nom d'agent, et ne montre l'adresse qu'aux endroits où elle sert
|
|
442
|
-
vraiment :
|
|
443
|
-
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
|
|
444
455
|
l'agent est suivi de celui de la boîte entre parenthèses — sans quoi un fil se lirait
|
|
445
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
|
|
446
457
|
« jamais connectée » — elle reste listée, puisqu'elle a du courrier en attente.
|
|
@@ -630,7 +641,7 @@ test/ # store, config, API HTTP, outils MCP, binaire réel
|
|
|
630
641
|
|
|
631
642
|
### Tests
|
|
632
643
|
|
|
633
|
-
|
|
644
|
+
287 tests couvrant : les suppressions de boîtes commandées depuis la console — une boîte
|
|
634
645
|
désignée à la main, les vides, celles sans activité depuis une date — avec leur aperçu,
|
|
635
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
|
|
636
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,11 @@ const state = {
|
|
|
52
53
|
filter: "all",
|
|
53
54
|
search: "",
|
|
54
55
|
threadId: null,
|
|
55
|
-
/**
|
|
56
|
-
|
|
56
|
+
/**
|
|
57
|
+
* Messages dépliés dans le lecteur, par identifiant. Vide au départ : un fil s'ouvre
|
|
58
|
+
* replié, et se parcourt avant de se lire.
|
|
59
|
+
*/
|
|
60
|
+
openMessages: new Set(),
|
|
57
61
|
collapsed: false,
|
|
58
62
|
/** Annuaire des boîtes, indexé par adresse : rempli par chaque `/admin/overview`. */
|
|
59
63
|
directory: new Map(),
|
|
@@ -90,7 +94,8 @@ const el = {
|
|
|
90
94
|
chips: document.getElementById("chips"),
|
|
91
95
|
threads: document.getElementById("threads"),
|
|
92
96
|
reader: document.getElementById("reader"),
|
|
93
|
-
|
|
97
|
+
details: document.getElementById("details"),
|
|
98
|
+
detailsBody: document.getElementById("details-body"),
|
|
94
99
|
purge: document.getElementById("purge"),
|
|
95
100
|
purgeOpen: document.getElementById("purge-open"),
|
|
96
101
|
purgeClose: document.getElementById("purge-close"),
|
|
@@ -457,9 +462,6 @@ function select(kind, key) {
|
|
|
457
462
|
if (isSelected(kind, key)) return;
|
|
458
463
|
state.selection = { kind, key };
|
|
459
464
|
state.threadId = null;
|
|
460
|
-
state.messageId = null;
|
|
461
|
-
el.sheet.hidden = true;
|
|
462
|
-
el.console.classList.remove("has-sheet");
|
|
463
465
|
refresh();
|
|
464
466
|
}
|
|
465
467
|
|
|
@@ -869,29 +871,81 @@ function renderThread(data) {
|
|
|
869
871
|
el.reader.replaceChildren(container);
|
|
870
872
|
}
|
|
871
873
|
|
|
874
|
+
/**
|
|
875
|
+
* Un message du fil, replié ou déplié.
|
|
876
|
+
*
|
|
877
|
+
* Un fil se relit d'abord de haut en bas : ce qui doit sauter aux yeux est qui a écrit,
|
|
878
|
+
* quand, et sur quoi. Les corps sont donc repliés au départ, chaque message tenant sur sa
|
|
879
|
+
* ligne de titre, et c'est cette ligne qui l'ouvre. L'état est retenu par identifiant de
|
|
880
|
+
* message : le rafraîchissement automatique ne referme donc pas ce qu'on est en train de
|
|
881
|
+
* lire.
|
|
882
|
+
*/
|
|
872
883
|
function messageRow(message, isLast) {
|
|
873
884
|
const item = node("li");
|
|
885
|
+
fillMessage(item, message, isLast);
|
|
886
|
+
return item;
|
|
887
|
+
}
|
|
888
|
+
|
|
889
|
+
/**
|
|
890
|
+
* Remplit la ligne d'un message, dépliée ou non.
|
|
891
|
+
*
|
|
892
|
+
* Le nœud est donné plutôt que rendu : ouvrir un message ne redessine que lui, et c'est
|
|
893
|
+
* bien cette ligne-là, celle qui est dans la page, qu'il faut réécrire — en reconstruire
|
|
894
|
+
* une autre reviendrait à modifier un élément que personne ne regarde.
|
|
895
|
+
*/
|
|
896
|
+
function fillMessage(item, message, isLast) {
|
|
897
|
+
const open = state.openMessages.has(message.message_id);
|
|
874
898
|
const classes = ["message"];
|
|
875
|
-
if (
|
|
876
|
-
|
|
899
|
+
if (open) classes.push("is-expanded");
|
|
900
|
+
if (isLast) classes.push("is-last");
|
|
877
901
|
const row = node("div", classes.join(" "));
|
|
878
|
-
|
|
879
|
-
row.appendChild(agentAvatar(message.from));
|
|
880
|
-
|
|
881
902
|
const column = node("div", "message-body-col");
|
|
882
|
-
|
|
883
|
-
|
|
884
|
-
|
|
885
|
-
|
|
886
|
-
|
|
887
|
-
|
|
903
|
+
|
|
904
|
+
/*
|
|
905
|
+
Tout ce qui est visible d'un message replié est cliquable, pastille comprise : le
|
|
906
|
+
bandeau est le bouton, et le survol le grise pour qu'on le voie comme tel. Pas de
|
|
907
|
+
chevron — sur une ligne qui s'annonce déjà comme une cible, il n'apprend rien.
|
|
908
|
+
*/
|
|
909
|
+
const head = node("button", "message-head");
|
|
910
|
+
head.type = "button";
|
|
911
|
+
head.setAttribute("aria-expanded", open ? "true" : "false");
|
|
912
|
+
head.appendChild(agentAvatar(message.from));
|
|
913
|
+
|
|
914
|
+
const identity = node("span", "message-identity");
|
|
915
|
+
identity.appendChild(node("span", "message-from", party(message.from).name));
|
|
916
|
+
if (message.priority === "high") identity.appendChild(tag("tag tag--high", "priorité haute"));
|
|
917
|
+
if (message.priority === "low") identity.appendChild(tag("tag", "priorité basse"));
|
|
918
|
+
if (!message.read) {
|
|
919
|
+
const dot = node("span", "message-dot");
|
|
920
|
+
dot.title = `non lu par ${party(message.to).name}`;
|
|
921
|
+
identity.appendChild(dot);
|
|
922
|
+
}
|
|
923
|
+
identity.appendChild(node("span", "message-time", formatShort(message.created_at)));
|
|
924
|
+
identity.appendChild(detailsButton(message));
|
|
925
|
+
head.appendChild(identity);
|
|
926
|
+
|
|
927
|
+
// L'objet, toujours : c'est le titre du message, et c'est lui qu'on cherche du regard.
|
|
928
|
+
// Le bandeau ne bouge donc pas quand le message s'ouvre, seul le corps vient dessous.
|
|
929
|
+
head.appendChild(node("span", "message-subject", message.subject || "sans objet"));
|
|
930
|
+
|
|
931
|
+
head.addEventListener("click", () => {
|
|
932
|
+
if (open) state.openMessages.delete(message.message_id);
|
|
933
|
+
else state.openMessages.add(message.message_id);
|
|
934
|
+
fillMessage(item, message, isLast);
|
|
935
|
+
});
|
|
888
936
|
column.appendChild(head);
|
|
889
937
|
|
|
890
|
-
|
|
891
|
-
|
|
892
|
-
|
|
938
|
+
if (!open) {
|
|
939
|
+
row.appendChild(column);
|
|
940
|
+
item.replaceChildren(row);
|
|
941
|
+
return;
|
|
893
942
|
}
|
|
894
|
-
|
|
943
|
+
|
|
944
|
+
// Ce qui suit la ligne de titre s'aligne sous le texte, pas sous la pastille.
|
|
945
|
+
const content = node("div", "message-content");
|
|
946
|
+
column.appendChild(content);
|
|
947
|
+
|
|
948
|
+
content.appendChild(messageText(message));
|
|
895
949
|
|
|
896
950
|
const foot = node("div", "message-foot");
|
|
897
951
|
foot.appendChild(
|
|
@@ -899,18 +953,10 @@ function messageRow(message, isLast) {
|
|
|
899
953
|
? tag("tag tag--ok", `lu par ${party(message.to).name}`, "check")
|
|
900
954
|
: tag("tag tag--warn", `non lu par ${party(message.to).name}`, "clock")
|
|
901
955
|
);
|
|
902
|
-
|
|
903
|
-
foot.appendChild(
|
|
904
|
-
button("btn btn--link", open ? "détails ouverts" : "détails", () => {
|
|
905
|
-
if (open) closeSheet();
|
|
906
|
-
else openMessage(message.message_id);
|
|
907
|
-
})
|
|
908
|
-
);
|
|
909
|
-
column.appendChild(foot);
|
|
956
|
+
content.appendChild(foot);
|
|
910
957
|
|
|
911
958
|
row.appendChild(column);
|
|
912
|
-
item.
|
|
913
|
-
return item;
|
|
959
|
+
item.replaceChildren(row);
|
|
914
960
|
}
|
|
915
961
|
|
|
916
962
|
/**
|
|
@@ -925,44 +971,82 @@ function messageText(message) {
|
|
|
925
971
|
return container;
|
|
926
972
|
}
|
|
927
973
|
|
|
928
|
-
// ---
|
|
974
|
+
// --- Détails d'un message --------------------------------------------------
|
|
929
975
|
|
|
930
|
-
|
|
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
|
+
}
|
|
1007
|
+
|
|
1008
|
+
/** Tout ce que la conversation ne montre pas, et que la supervision peut vouloir lire. */
|
|
1009
|
+
function renderDetails(message) {
|
|
931
1010
|
const container = document.createDocumentFragment();
|
|
932
1011
|
|
|
933
|
-
const head = node("div", "
|
|
1012
|
+
const head = node("div", "modal-head");
|
|
934
1013
|
const titles = node("div");
|
|
935
|
-
titles.appendChild(node("
|
|
936
|
-
titles.appendChild(node("p", "
|
|
1014
|
+
titles.appendChild(node("h2", null, "Détails du message"));
|
|
1015
|
+
titles.appendChild(node("p", "details-subject", message.subject));
|
|
937
1016
|
head.appendChild(titles);
|
|
938
1017
|
const close = node("button", "btn btn--icon btn--ghost");
|
|
939
1018
|
close.type = "button";
|
|
940
1019
|
close.title = "Fermer";
|
|
941
|
-
close.setAttribute("aria-label", "Fermer
|
|
1020
|
+
close.setAttribute("aria-label", "Fermer les détails");
|
|
942
1021
|
close.appendChild(icon("close"));
|
|
943
|
-
close.addEventListener("click",
|
|
1022
|
+
close.addEventListener("click", () => el.details.close());
|
|
944
1023
|
head.appendChild(close);
|
|
945
1024
|
container.appendChild(head);
|
|
946
1025
|
|
|
947
|
-
const ids = node("div", "
|
|
1026
|
+
const ids = node("div", "details-section");
|
|
948
1027
|
ids.appendChild(node("span", "section-label", "Message"));
|
|
949
1028
|
ids.appendChild(kvCopy("Identifiant", message.message_id));
|
|
950
|
-
ids.appendChild(
|
|
1029
|
+
ids.appendChild(kv("Objet", node("span", "kv-value", message.subject)));
|
|
951
1030
|
ids.appendChild(kv("Envoyé le", node("span", "kv-value", formatDate(message.created_at))));
|
|
952
1031
|
ids.appendChild(kv("Priorité", priorityTag(message.priority)));
|
|
953
1032
|
container.appendChild(ids);
|
|
954
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
|
+
|
|
955
1041
|
container.appendChild(partySection("De", message.from));
|
|
956
1042
|
container.appendChild(partySection("À", message.to));
|
|
957
1043
|
|
|
958
|
-
const deliveries = node("div", "
|
|
1044
|
+
const deliveries = node("div", "details-section");
|
|
959
1045
|
deliveries.appendChild(node("span", "section-label", "Distribution"));
|
|
960
1046
|
deliveries.appendChild(deliveriesTable(message.deliveries || []));
|
|
961
1047
|
container.appendChild(deliveries);
|
|
962
1048
|
|
|
963
|
-
el.
|
|
964
|
-
el.sheet.hidden = false;
|
|
965
|
-
el.console.classList.add("has-sheet");
|
|
1049
|
+
el.detailsBody.replaceChildren(container);
|
|
966
1050
|
}
|
|
967
1051
|
|
|
968
1052
|
function kv(key, valueNode) {
|
|
@@ -992,7 +1076,7 @@ function priorityTag(priority) {
|
|
|
992
1076
|
*/
|
|
993
1077
|
function partySection(label, agentId) {
|
|
994
1078
|
const who = party(agentId);
|
|
995
|
-
const section = node("div", "
|
|
1079
|
+
const section = node("div", "details-section");
|
|
996
1080
|
section.appendChild(node("span", "section-label", label));
|
|
997
1081
|
|
|
998
1082
|
const block = node("div", "party");
|
|
@@ -1034,21 +1118,13 @@ function deliveriesTable(deliveries) {
|
|
|
1034
1118
|
return table;
|
|
1035
1119
|
}
|
|
1036
1120
|
|
|
1037
|
-
function closeSheet() {
|
|
1038
|
-
state.messageId = null;
|
|
1039
|
-
el.sheet.hidden = true;
|
|
1040
|
-
el.sheet.replaceChildren();
|
|
1041
|
-
el.console.classList.remove("has-sheet");
|
|
1042
|
-
if (state.threadId) refreshThread();
|
|
1043
|
-
}
|
|
1044
|
-
|
|
1045
1121
|
// --- Navigation ------------------------------------------------------------
|
|
1046
1122
|
|
|
1047
1123
|
async function openThread(threadId) {
|
|
1048
1124
|
state.threadId = threadId;
|
|
1049
|
-
|
|
1050
|
-
|
|
1051
|
-
|
|
1125
|
+
// Un fil s'ouvre replié, même celui qu'on rouvre : ce qui était déplié l'était pour
|
|
1126
|
+
// l'autre conversation.
|
|
1127
|
+
state.openMessages.clear();
|
|
1052
1128
|
await refreshThread();
|
|
1053
1129
|
await refresh({ keepThread: true });
|
|
1054
1130
|
}
|
|
@@ -1065,16 +1141,6 @@ async function refreshThread() {
|
|
|
1065
1141
|
}
|
|
1066
1142
|
}
|
|
1067
1143
|
|
|
1068
|
-
async function openMessage(messageId) {
|
|
1069
|
-
state.messageId = messageId;
|
|
1070
|
-
try {
|
|
1071
|
-
renderSheet(await api(`/admin/messages/${encodeURIComponent(messageId)}`));
|
|
1072
|
-
await refreshThread();
|
|
1073
|
-
} catch (error) {
|
|
1074
|
-
if (error.message !== "unauthorized") el.sheet.replaceChildren(node("div", "banner", error.message));
|
|
1075
|
-
}
|
|
1076
|
-
}
|
|
1077
|
-
|
|
1078
1144
|
// --- Boucle de rafraîchissement --------------------------------------------
|
|
1079
1145
|
|
|
1080
1146
|
async function refresh(options) {
|
|
@@ -1112,14 +1178,13 @@ async function refresh(options) {
|
|
|
1112
1178
|
/**
|
|
1113
1179
|
* Ce que chaque séparateur commande.
|
|
1114
1180
|
*
|
|
1115
|
-
* `edge` dit de quel côté se trouve le panneau réglé :
|
|
1116
|
-
*
|
|
1117
|
-
*
|
|
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.
|
|
1118
1184
|
*/
|
|
1119
1185
|
const PANELS = {
|
|
1120
1186
|
"resize-agents": { name: "--w-agents", panel: "agentsPane", edge: "before", min: 180, max: 520, initial: 240 },
|
|
1121
1187
|
"resize-threads": { name: "--w-threads", panel: "threadsPane", edge: "before", min: 260, max: 720, initial: 400 },
|
|
1122
|
-
"resize-sheet": { name: "--w-sheet", panel: "sheet", edge: "after", min: 280, max: 720, initial: 360 },
|
|
1123
1188
|
};
|
|
1124
1189
|
|
|
1125
1190
|
const LAYOUT_KEY = "messenger.layout";
|
|
@@ -1396,9 +1461,6 @@ async function confirmPurge() {
|
|
|
1396
1461
|
state.selection = { kind: "all", key: null };
|
|
1397
1462
|
}
|
|
1398
1463
|
state.threadId = null;
|
|
1399
|
-
state.messageId = null;
|
|
1400
|
-
el.sheet.hidden = true;
|
|
1401
|
-
el.console.classList.remove("has-sheet");
|
|
1402
1464
|
await refresh();
|
|
1403
1465
|
} catch (error) {
|
|
1404
1466
|
purge.armed = false;
|
|
@@ -1486,11 +1548,9 @@ el.logout.addEventListener("click", async () => {
|
|
|
1486
1548
|
state.expanded = new Set();
|
|
1487
1549
|
state.showEmpty = new Set();
|
|
1488
1550
|
state.threadId = null;
|
|
1489
|
-
state.
|
|
1551
|
+
state.openMessages = new Set();
|
|
1490
1552
|
state.directory = new Map();
|
|
1491
1553
|
state.groups = [];
|
|
1492
|
-
el.sheet.hidden = true;
|
|
1493
|
-
el.console.classList.remove("has-sheet");
|
|
1494
1554
|
el.agents.replaceChildren();
|
|
1495
1555
|
el.threads.replaceChildren();
|
|
1496
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
|
-
<!--
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
<
|
|
119
|
-
<
|
|
120
|
-
|
|
121
|
-
<
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
</span>
|
|
129
|
-
<span class="field-
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
<
|
|
153
|
-
|
|
154
|
-
|
|
155
|
-
|
|
156
|
-
|
|
157
|
-
|
|
158
|
-
|
|
159
|
-
|
|
160
|
-
|
|
161
|
-
class="
|
|
162
|
-
id="
|
|
163
|
-
|
|
164
|
-
|
|
165
|
-
|
|
166
|
-
|
|
167
|
-
|
|
168
|
-
|
|
169
|
-
<
|
|
170
|
-
|
|
171
|
-
|
|
172
|
-
|
|
173
|
-
|
|
174
|
-
|
|
175
|
-
|
|
176
|
-
|
|
177
|
-
|
|
178
|
-
|
|
179
|
-
|
|
180
|
-
|
|
181
|
-
|
|
182
|
-
|
|
183
|
-
|
|
184
|
-
|
|
185
|
-
<
|
|
186
|
-
|
|
187
|
-
|
|
188
|
-
|
|
189
|
-
|
|
190
|
-
|
|
191
|
-
|
|
192
|
-
|
|
193
|
-
|
|
194
|
-
|
|
195
|
-
|
|
196
|
-
|
|
197
|
-
|
|
198
|
-
|
|
199
|
-
|
|
200
|
-
<
|
|
201
|
-
|
|
202
|
-
|
|
203
|
-
|
|
204
|
-
|
|
205
|
-
|
|
206
|
-
|
|
207
|
-
|
|
208
|
-
|
|
209
|
-
|
|
210
|
-
|
|
211
|
-
|
|
212
|
-
|
|
213
|
-
|
|
214
|
-
|
|
215
|
-
|
|
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
|
|
|
@@ -992,11 +973,39 @@ body {
|
|
|
992
973
|
padding-left: 8px;
|
|
993
974
|
}
|
|
994
975
|
|
|
995
|
-
/*
|
|
996
|
-
|
|
976
|
+
/*
|
|
977
|
+
Colonne repliée, deux signes distincts et souvent simultanés.
|
|
978
|
+
|
|
979
|
+
L'anneau dit ce qui est **sélectionné** : c'est un contour, il entoure, il désigne — la
|
|
980
|
+
teinte de fond qui sert à ça dans la colonne dépliée n'a plus de place ici. La pastille
|
|
981
|
+
dit qu'il reste du **non-lu** : un point posé sur l'avatar, la même convention que la
|
|
982
|
+
liste des fils. L'un n'implique pas l'autre, ils doivent donc se lire séparément.
|
|
983
|
+
*/
|
|
984
|
+
.is-collapsed .agent.is-selected {
|
|
985
|
+
background: none;
|
|
986
|
+
}
|
|
987
|
+
|
|
988
|
+
.is-collapsed .agent.is-selected .avatar {
|
|
997
989
|
box-shadow: 0 0 0 2px var(--bg), 0 0 0 4px var(--accent);
|
|
998
990
|
}
|
|
999
991
|
|
|
992
|
+
.is-collapsed .agent.has-unread .avatar {
|
|
993
|
+
position: relative;
|
|
994
|
+
}
|
|
995
|
+
|
|
996
|
+
.is-collapsed .agent.has-unread .avatar::after {
|
|
997
|
+
content: "";
|
|
998
|
+
position: absolute;
|
|
999
|
+
top: -1px;
|
|
1000
|
+
right: -1px;
|
|
1001
|
+
width: 9px;
|
|
1002
|
+
height: 9px;
|
|
1003
|
+
border-radius: 50%;
|
|
1004
|
+
background: var(--accent);
|
|
1005
|
+
/* Le liseré détache la pastille de l'avatar, quelle que soit la couleur de celui-ci. */
|
|
1006
|
+
box-shadow: 0 0 0 2px var(--bg);
|
|
1007
|
+
}
|
|
1008
|
+
|
|
1000
1009
|
.is-collapsed .agents {
|
|
1001
1010
|
overflow-x: hidden;
|
|
1002
1011
|
}
|
|
@@ -1320,14 +1329,25 @@ body {
|
|
|
1320
1329
|
.messages {
|
|
1321
1330
|
list-style: none;
|
|
1322
1331
|
margin: 0;
|
|
1323
|
-
|
|
1332
|
+
/* Le retrait latéral du texte vient d'ici plus du cadre d'un message : 20 + 12. */
|
|
1333
|
+
padding: 12px 20px 40px;
|
|
1334
|
+
}
|
|
1335
|
+
|
|
1336
|
+
/*
|
|
1337
|
+
Un trait entre deux messages, et non un simple blanc.
|
|
1338
|
+
|
|
1339
|
+
Repliés, les messages se suivent de près : sans démarcation, on devine où l'un s'arrête
|
|
1340
|
+
à la seule présence de la pastille suivante. Le trait court sur toute la largeur de la
|
|
1341
|
+
liste, et l'écart de part et d'autre vient du cadre des messages eux-mêmes.
|
|
1342
|
+
*/
|
|
1343
|
+
.messages > li + li {
|
|
1344
|
+
border-top-width: 1px;
|
|
1345
|
+
border-top-style: solid;
|
|
1346
|
+
border-top-color: var(--border);
|
|
1324
1347
|
}
|
|
1325
1348
|
|
|
1326
1349
|
.message {
|
|
1327
|
-
display: flex;
|
|
1328
|
-
gap: 12px;
|
|
1329
1350
|
padding: 12px;
|
|
1330
|
-
margin: 0 -12px;
|
|
1331
1351
|
border-radius: var(--radius);
|
|
1332
1352
|
}
|
|
1333
1353
|
|
|
@@ -1342,24 +1362,111 @@ body {
|
|
|
1342
1362
|
}
|
|
1343
1363
|
|
|
1344
1364
|
.message-body-col {
|
|
1345
|
-
flex: 1;
|
|
1346
1365
|
min-width: 0;
|
|
1347
1366
|
}
|
|
1348
1367
|
|
|
1368
|
+
/*
|
|
1369
|
+
Le bandeau qui ouvre et referme le message.
|
|
1370
|
+
|
|
1371
|
+
Il tient tout ce qu'un message replié montre : la pastille, qui écrit à qui, l'heure, et
|
|
1372
|
+
la ligne de résumé. Rien n'y ressemble à un bouton tant qu'on ne le survole pas — c'est
|
|
1373
|
+
le fond gris qui dit alors que la zone entière est une cible, mieux qu'un chevron.
|
|
1374
|
+
|
|
1375
|
+
Deux colonnes, deux lignes : la pastille tient sur les deux, le texte occupe la seconde
|
|
1376
|
+
colonne — qui écrit à qui sur la première ligne, l'objet du message sur la seconde.
|
|
1377
|
+
*/
|
|
1349
1378
|
.message-head {
|
|
1379
|
+
display: grid;
|
|
1380
|
+
grid-template-columns: auto minmax(0, 1fr);
|
|
1381
|
+
align-items: center;
|
|
1382
|
+
gap: 2px 12px;
|
|
1383
|
+
width: 100%;
|
|
1384
|
+
padding: 6px;
|
|
1385
|
+
margin: -6px;
|
|
1386
|
+
border: 0;
|
|
1387
|
+
border-radius: var(--radius);
|
|
1388
|
+
background: none;
|
|
1389
|
+
font: inherit;
|
|
1390
|
+
color: inherit;
|
|
1391
|
+
text-align: left;
|
|
1392
|
+
cursor: pointer;
|
|
1393
|
+
}
|
|
1394
|
+
|
|
1395
|
+
.message-head:hover {
|
|
1396
|
+
background: var(--neutral-soft);
|
|
1397
|
+
}
|
|
1398
|
+
|
|
1399
|
+
.message-head:focus-visible {
|
|
1400
|
+
outline: 2px solid var(--accent);
|
|
1401
|
+
outline-offset: -2px;
|
|
1402
|
+
}
|
|
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 {
|
|
1350
1410
|
display: flex;
|
|
1351
|
-
|
|
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
|
+
|
|
1428
|
+
.message-head > .avatar {
|
|
1429
|
+
grid-row: span 2;
|
|
1430
|
+
}
|
|
1431
|
+
|
|
1432
|
+
/* Qui écrit à qui, et quand : l'heure est poussée au bout de la ligne. */
|
|
1433
|
+
.message-identity {
|
|
1434
|
+
display: flex;
|
|
1435
|
+
align-items: center;
|
|
1352
1436
|
gap: 8px;
|
|
1353
1437
|
flex-wrap: wrap;
|
|
1438
|
+
min-width: 0;
|
|
1354
1439
|
}
|
|
1355
1440
|
|
|
1356
|
-
|
|
1357
|
-
|
|
1441
|
+
/*
|
|
1442
|
+
L'objet du message, sur sa ligne, replié comme déplié : c'est ce qui identifie le
|
|
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.
|
|
1446
|
+
*/
|
|
1447
|
+
.message-subject {
|
|
1448
|
+
min-width: 0;
|
|
1449
|
+
white-space: nowrap;
|
|
1450
|
+
overflow: hidden;
|
|
1451
|
+
text-overflow: ellipsis;
|
|
1358
1452
|
}
|
|
1359
1453
|
|
|
1360
|
-
.
|
|
1361
|
-
|
|
1362
|
-
|
|
1454
|
+
/* Le corps s'aligne sous le texte du bandeau, jamais sous la pastille. */
|
|
1455
|
+
.message-content {
|
|
1456
|
+
padding-left: 40px;
|
|
1457
|
+
}
|
|
1458
|
+
|
|
1459
|
+
/* Non lu : le même point que dans la liste des fils, et non un anneau ni une couleur. */
|
|
1460
|
+
.message-dot {
|
|
1461
|
+
width: 8px;
|
|
1462
|
+
height: 8px;
|
|
1463
|
+
border-radius: 50%;
|
|
1464
|
+
background: var(--accent);
|
|
1465
|
+
flex: none;
|
|
1466
|
+
}
|
|
1467
|
+
|
|
1468
|
+
.message-from {
|
|
1469
|
+
font-weight: 600;
|
|
1363
1470
|
}
|
|
1364
1471
|
|
|
1365
1472
|
.message-time {
|
|
@@ -1369,15 +1476,8 @@ body {
|
|
|
1369
1476
|
white-space: nowrap;
|
|
1370
1477
|
}
|
|
1371
1478
|
|
|
1372
|
-
.message-subject {
|
|
1373
|
-
margin: 8px 0 0;
|
|
1374
|
-
font-size: 13px;
|
|
1375
|
-
font-weight: 600;
|
|
1376
|
-
color: var(--muted);
|
|
1377
|
-
}
|
|
1378
|
-
|
|
1379
1479
|
.message-text {
|
|
1380
|
-
margin:
|
|
1480
|
+
margin: 8px 0 0;
|
|
1381
1481
|
white-space: pre-wrap;
|
|
1382
1482
|
overflow-wrap: anywhere;
|
|
1383
1483
|
}
|
|
@@ -1539,48 +1639,26 @@ body {
|
|
|
1539
1639
|
font-size: 12px;
|
|
1540
1640
|
}
|
|
1541
1641
|
|
|
1542
|
-
/* ---
|
|
1543
|
-
|
|
1544
|
-
.sheet {
|
|
1545
|
-
border-left: 1px solid var(--border);
|
|
1546
|
-
background: var(--panel);
|
|
1547
|
-
overflow-y: auto;
|
|
1548
|
-
min-width: 0;
|
|
1549
|
-
}
|
|
1550
|
-
|
|
1551
|
-
.sheet-head {
|
|
1552
|
-
display: flex;
|
|
1553
|
-
align-items: flex-start;
|
|
1554
|
-
gap: 8px;
|
|
1555
|
-
padding: 18px 20px 14px;
|
|
1556
|
-
border-bottom: 1px solid var(--border);
|
|
1557
|
-
}
|
|
1558
|
-
|
|
1559
|
-
.sheet-head h3 {
|
|
1560
|
-
margin: 0;
|
|
1561
|
-
font-size: 15px;
|
|
1562
|
-
}
|
|
1642
|
+
/* --- Détails d'un message ------------------------------------------------- */
|
|
1563
1643
|
|
|
1564
|
-
.
|
|
1644
|
+
/* L'objet, sous le titre de la modale : il dit de quel message on lit les détails. */
|
|
1645
|
+
.details-subject {
|
|
1565
1646
|
margin: 4px 0 0;
|
|
1566
1647
|
font-size: 13px;
|
|
1567
1648
|
color: var(--muted);
|
|
1568
1649
|
}
|
|
1569
1650
|
|
|
1570
|
-
.
|
|
1571
|
-
|
|
1572
|
-
|
|
1573
|
-
|
|
1574
|
-
.sheet-section {
|
|
1575
|
-
padding: 16px 20px;
|
|
1576
|
-
border-bottom: 1px solid var(--border);
|
|
1651
|
+
.details-section {
|
|
1652
|
+
padding: 16px 0;
|
|
1653
|
+
border-bottom: 1px solid var(--border-soft);
|
|
1577
1654
|
}
|
|
1578
1655
|
|
|
1579
|
-
.
|
|
1656
|
+
.details-section:last-of-type {
|
|
1580
1657
|
border-bottom: 0;
|
|
1658
|
+
padding-bottom: 0;
|
|
1581
1659
|
}
|
|
1582
1660
|
|
|
1583
|
-
.
|
|
1661
|
+
.details-section .section-label {
|
|
1584
1662
|
display: block;
|
|
1585
1663
|
margin-bottom: 12px;
|
|
1586
1664
|
}
|
|
@@ -1717,13 +1795,6 @@ body {
|
|
|
1717
1795
|
|
|
1718
1796
|
/* --- Écrans étroits ------------------------------------------------------- */
|
|
1719
1797
|
|
|
1720
|
-
@media (max-width: 1200px) {
|
|
1721
|
-
.console.has-sheet,
|
|
1722
|
-
.console.is-collapsed.has-sheet {
|
|
1723
|
-
grid-template-columns: 57px 320px minmax(0, 1fr) 320px;
|
|
1724
|
-
}
|
|
1725
|
-
}
|
|
1726
|
-
|
|
1727
1798
|
@media (max-width: 1000px) {
|
|
1728
1799
|
.topbar-search {
|
|
1729
1800
|
max-width: 240px;
|
|
@@ -1731,22 +1802,6 @@ body {
|
|
|
1731
1802
|
|
|
1732
1803
|
.console,
|
|
1733
1804
|
.console.is-collapsed {
|
|
1734
|
-
grid-template-columns: 57px 300px minmax(0, 1fr);
|
|
1735
|
-
}
|
|
1736
|
-
|
|
1737
|
-
/* Trop étroit pour quatre colonnes : le volet recouvre le lecteur. */
|
|
1738
|
-
.console.has-sheet,
|
|
1739
|
-
.console.is-collapsed.has-sheet {
|
|
1740
|
-
grid-template-columns: 57px 300px minmax(0, 1fr);
|
|
1741
|
-
}
|
|
1742
|
-
|
|
1743
|
-
.console.has-sheet .sheet {
|
|
1744
|
-
position: fixed;
|
|
1745
|
-
top: var(--topbar-h);
|
|
1746
|
-
right: 0;
|
|
1747
|
-
bottom: 0;
|
|
1748
|
-
width: 340px;
|
|
1749
|
-
max-width: 100%;
|
|
1750
|
-
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);
|
|
1751
1806
|
}
|
|
1752
1807
|
}
|
package/dist/src/version.d.ts
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
export declare const APP_VERSION = "1.0.
|
|
1
|
+
export declare const APP_VERSION = "1.0.25";
|
package/dist/src/version.js
CHANGED
|
@@ -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.
|
|
2
|
+
export const APP_VERSION = "1.0.25";
|