hmgr 0.0.4 → 0.0.6

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.
Files changed (41) hide show
  1. package/README.md +10 -0
  2. package/dist/adapter/checkout-pfad.js +10 -2
  3. package/dist/adapter/claude-code.js +251 -23
  4. package/dist/adapter/composer.js +4 -3
  5. package/dist/adapter/datei-text.js +18 -0
  6. package/dist/adapter/dateisystem.js +146 -8
  7. package/dist/adapter/git.js +129 -17
  8. package/dist/adapter/hermes.js +235 -10
  9. package/dist/adapter/marketplace-json.js +2 -1
  10. package/dist/adapter/mcp-katalog.js +124 -0
  11. package/dist/adapter/quelle-text.js +10 -1
  12. package/dist/adapter/skill-md.js +96 -19
  13. package/dist/adapter/terminal.js +1 -1
  14. package/dist/adapter/umgebung.js +43 -0
  15. package/dist/adapter/vergleich.js +20 -0
  16. package/dist/adapter/zustand-json.js +6 -1
  17. package/dist/cli/hilfe.js +227 -26
  18. package/dist/cli/main.js +362 -20
  19. package/dist/gui/beobachter.js +108 -0
  20. package/dist/gui/icons.js +114 -0
  21. package/dist/gui/login-pfad.js +120 -0
  22. package/dist/gui/main.js +823 -0
  23. package/dist/gui/mcp-ansicht.js +57 -0
  24. package/dist/gui/oeffnen.js +33 -0
  25. package/dist/gui/preload.cjs +70 -0
  26. package/dist/gui/renderer/fonts/Inter-LICENSE.txt +92 -0
  27. package/dist/gui/renderer/fonts/InterVariable.woff2 +0 -0
  28. package/dist/gui/renderer/fonts/JetBrainsMono-Medium.woff2 +0 -0
  29. package/dist/gui/renderer/fonts/JetBrainsMono-OFL.txt +93 -0
  30. package/dist/gui/renderer/fonts/JetBrainsMono-Regular.woff2 +0 -0
  31. package/dist/gui/renderer/index.html +581 -0
  32. package/dist/gui/renderer/renderer.js +3227 -0
  33. package/dist/gui/renderer/styles.css +701 -0
  34. package/dist/kern/anwendungsfaelle.js +859 -65
  35. package/dist/kern/modell.js +297 -0
  36. package/dist/kern/quelle-ort.js +14 -1
  37. package/dist/ports/harness.js +16 -0
  38. package/dist/ports/mcp-katalog.js +1 -0
  39. package/dist/ports/umgebung.js +1 -0
  40. package/dist/ports/zustand.js +1 -1
  41. package/package.json +47 -2
@@ -0,0 +1,3227 @@
1
+ // hmgr-GUI, Renderer. Kein Bundler, kein Framework: DOM direkt gebaut, Daten über window.hmgr (IPC).
2
+ (function () {
3
+ 'use strict';
4
+
5
+ /* ---------------- Kleinkram ---------------- */
6
+
7
+ function $(id) { return document.getElementById(id); }
8
+ function e(tag, klasse, text) {
9
+ var el = document.createElement(tag);
10
+ if (klasse) el.className = klasse;
11
+ if (text != null) el.textContent = text;
12
+ return el;
13
+ }
14
+
15
+ // Home-Verzeichnis und Plattform, einmal beim Laden über hmgr:home und hmgr:plattform geholt
16
+ // (siehe ladeUmgebung unten). Nur für die Anzeige: beides fließt nie in einen IPC-Aufruf oder
17
+ // einen zielKey zurück.
18
+ var HOME = '';
19
+ // Pfadtrenner der Anzeige (E68). Aus der Plattform und nicht aus dem Home-Pfad abgeleitet: die
20
+ // Pfade, die hier angezeigt werden, baut der Kern mit `node:path`, also mit dem Trenner der
21
+ // Plattform — auch dann, wenn HOME selbst in POSIX-Schreibweise gesetzt ist (MSYS/Git-Bash).
22
+ var TRENNER = '/';
23
+ /** Trenner einer Plattform, wie `process.platform` sie nennt. Rein, nur für die Anzeige. */
24
+ function plattformTrenner(plattform) { return plattform === 'win32' ? '\\' : '/'; }
25
+ /**
26
+ * Kürzt einen absoluten Pfad auf ~, wenn er mit dem Home-Verzeichnis beginnt. Grenze ist einer
27
+ * der beiden Trenner (E68): unter Windows steht dort „\", und der Rest des Pfads bleibt wörtlich
28
+ * stehen, die Kürzung heißt dort also von selbst `~\…` statt `~/…`.
29
+ */
30
+ function pfadKurz(p) {
31
+ if (!HOME || typeof p !== 'string') return p;
32
+ if (p === HOME) return '~';
33
+ var grenze = p.charAt(HOME.length);
34
+ if (p.indexOf(HOME) === 0 && (grenze === '/' || grenze === '\\')) return '~' + p.slice(HOME.length);
35
+ return p;
36
+ }
37
+ /**
38
+ * Schreibt einen fest im Quelltext stehenden POSIX-Pfad in der Schreibweise der Plattform (E68),
39
+ * damit `~/.claude.json` unter Windows nicht neben `~\Code\…` steht. Nur für Anzeigetexte, die
40
+ * hmgr selbst formuliert — nie für einen Pfad, der aus dem Kern kommt.
41
+ */
42
+ function pfadSchreibweise(p) { return TRENNER === '/' ? p : p.split('/').join(TRENNER); }
43
+
44
+ function melde(text) {
45
+ $('meldungtext').textContent = text;
46
+ $('meldung').hidden = false;
47
+ }
48
+ $('meldungzu').addEventListener('click', function () { $('meldung').hidden = true; });
49
+
50
+ /**
51
+ * Hinweiszeile im Erfolgsdialog (E26, F32). Jeder Aufruf, der d-erfolg füllt, ruft das auf, auch
52
+ * mit leerer Liste: sonst stünde der Hinweis des vorigen Laufs noch da. Doppelte Hinweise stehen
53
+ * einmal, ein Lauf über mehrere Skills sagt dieselbe Sache sonst N-mal.
54
+ */
55
+ function setzeErfolgHinweise(hinweise) {
56
+ var einmalig = [];
57
+ (hinweise || []).forEach(function (h) {
58
+ if (h && einmalig.indexOf(h) < 0) einmalig.push(h);
59
+ });
60
+ $('de-hinweis').textContent = einmalig.join('\n');
61
+ $('de-hinweis').hidden = einmalig.length === 0;
62
+ }
63
+
64
+ /** Ruft einen window.hmgr-Kanal auf, meldet einen Fehler in der Leiste statt in der Konsole. */
65
+ async function rufe(fn) {
66
+ var ergebnis = await fn();
67
+ if (!ergebnis.ok) {
68
+ melde(ergebnis.fehler);
69
+ throw new Error(ergebnis.fehler);
70
+ }
71
+ return ergebnis.daten;
72
+ }
73
+
74
+ /* ---------------- Dialoge ---------------- */
75
+
76
+ var fokusStapel = [];
77
+ function zeige(dlg) {
78
+ fokusStapel.push(document.activeElement);
79
+ dlg.returnValue = '';
80
+ dlg.showModal();
81
+ }
82
+ document.querySelectorAll('dialog').forEach(function (dlg) {
83
+ dlg.addEventListener('close', function () {
84
+ var ziel = fokusStapel.pop();
85
+ if (ziel && ziel.focus && ziel.isConnected) ziel.focus();
86
+ });
87
+ });
88
+ function nachDialog(dlg, fn) {
89
+ dlg.addEventListener('close', function () { fn(dlg.returnValue); });
90
+ }
91
+ // Enter im einzigen Textfeld meint Bestätigen. Ohne das drückt die implizite Formular-Absendung
92
+ // den ersten Submit-Knopf, und der ist in den Dialogen „Abbrechen".
93
+ function enterBestaetigt(feld, knopf) {
94
+ feld.addEventListener('keydown', function (ev) {
95
+ if (ev.key !== 'Enter') return;
96
+ ev.preventDefault();
97
+ knopf.click();
98
+ });
99
+ }
100
+
101
+ /* ---------------- Harness-Zeichen ---------------- */
102
+
103
+ // Originalzeichen der Harnesses: Claude-Stern (Anthropics Claude-Symbol, Pfad aus simple-icons,
104
+ // identisch mit dem offiziellen Logo-SVG; Füllpfad, kein Strich), Agent-Skills-Hexagon (Favicon
105
+ // von agentskills.io, dem Standard hinter ~/.agents). Beide einfarbig über currentColor.
106
+ var HARNESS_ZEICHEN = {
107
+ 'claude-code': { viewBox: '0 0 24 24', inhalt: '<path d="m4.7144 15.9555 4.7174-2.6471.079-.2307-.079-.1275h-.2307l-.7893-.0486-2.6956-.0729-2.3375-.0971-2.2646-.1214-.5707-.1215-.5343-.7042.0546-.3522.4797-.3218.686.0608 1.5179.1032 2.2767.1578 1.6514.0972 2.4468.255h.3886l.0546-.1579-.1336-.0971-.1032-.0972L6.973 9.8356l-2.55-1.6879-1.3356-.9714-.7225-.4918-.3643-.4614-.1578-1.0078.6557-.7225.8803.0607.2246.0607.8925.686 1.9064 1.4754 2.4893 1.8336.3643.3035.1457-.1032.0182-.0728-.164-.2733-1.3539-2.4467-1.445-2.4893-.6435-1.032-.17-.6194c-.0607-.255-.1032-.4674-.1032-.7285L6.287.1335 6.6997 0l.9957.1336.419.3642.6192 1.4147 1.0018 2.2282 1.5543 3.0296.4553.8985.2429.8318.091.255h.1579v-.1457l.1275-1.706.2368-2.0947.2307-2.6957.0789-.7589.3764-.9107.7468-.4918.5828.2793.4797.686-.0668.4433-.2853 1.8517-.5586 2.9021-.3643 1.9429h.2125l.2429-.2429.9835-1.3053 1.6514-2.0643.7286-.8196.85-.9046.5464-.4311h1.0321l.759 1.1293-.34 1.1657-1.0625 1.3478-.8804 1.1414-1.2628 1.7-.7893 1.36.0729.1093.1882-.0183 2.8535-.607 1.5421-.2794 1.8396-.3157.8318.3886.091.3946-.3278.8075-1.967.4857-2.3072.4614-3.4364.8136-.0425.0304.0486.0607 1.5482.1457.6618.0364h1.621l3.0175.2247.7892.522.4736.6376-.079.4857-1.2142.6193-1.6393-.3886-3.825-.9107-1.3113-.3279h-.1822v.1093l1.0929 1.0686 2.0035 1.8092 2.5075 2.3314.1275.5768-.3218.4554-.34-.0486-2.2039-1.6575-.85-.7468-1.9246-1.621h-.1275v.17l.4432.6496 2.3436 3.5214.1214 1.0807-.17.3521-.6071.2125-.6679-.1214-1.3721-1.9246L14.38 17.959l-1.1414-1.9428-.1397.079-.674 7.2552-.3156.3703-.7286.2793-.6071-.4614-.3218-.7468.3218-1.4753.3886-1.9246.3157-1.53.2853-1.9004.17-.6314-.0121-.0425-.1397.0182-1.4328 1.9672-2.1796 2.9446-1.7243 1.8456-.4128.164-.7164-.3704.0667-.6618.4008-.5889 2.386-3.0357 1.4389-1.882.929-1.0868-.0062-.1579h-.0546l-6.3385 4.1164-1.1293.1457-.4857-.4554.0608-.7467.2307-.2429 1.9064-1.3114Z"/>' },
108
+ agents: { viewBox: '0 0 32 32', inhalt: '<path d="M16 0.5L29.4234 8.25V23.75L16 31.5L2.57661 23.75V8.25L16 0.5Z"/>' },
109
+ };
110
+ // Hermes hat kein reduzierbares Vektorzeichen (das Original ist eine Manga-Illustration); hier
111
+ // das 32px-Favicon aus Tims lokaler Hermes-Installation, weißer Grund inklusive.
112
+ var HARNESS_BILD = {
113
+ hermes: 'data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAACAAAAAgCAYAAABzenr0AAAKSUlEQVR4AYxXCXBNWRr+zn0v+yL2RpOtBT2tRULb1yiji+5B2UONaqbH0pIY+9aoooZuhLKvZeyjGU0mtoSyTQvVmCSSEEIwQSSRhOxvm+8/74momqmZW/e8e+655/zr9y/PmDdvXsrp06drzp8/b/l/xrlz5yyJiYmW48ePWw4dPmw5cuSvlhMnTljOnDnzwfmk/0FPeM6ePTvFaNeunREVFWXq37+/+T+PKHO/fv3MvXv3Nkd0jDCHtW5tdnP3MKenp5t379pl/uGH1eZNmzaZz5w5a66srDJHRnYy9+3bz9wvqj/pRXHI0zX0mmvev79JeBu8wEvJ0zAZMAznUErB4XCgsLAAlBYLFy7EjBkzsGbtWuT96xlGjRyFpUuXIjT0E9y8eRNbtmzGuLFjMHLkCCQm/h0V5RUgCRc9EzRt5aRtMpxPpRQMvLsUJw4O3sLYYrHg7NmzGDt2LCZMmIB18fE4fOQwNm/ejGnTpmHiNxNRWVmJrVu3YPr06RDByysqcPHiRUyeNEkL9+bNG1KTm4R5y0xGnSkMpRQlVQ5VZ7WwsBCrV63SjK9cuQJNiNaQw0opVFdXI/WfqYiJicGtX29h7ty5iI6Ohslk4haFUjI+ePAgDhw4AFGEi//1rrWA8Hc47MjPz8eiRYuwfsMGVFBDPz8/mM1mraFQcXd3R1hYGLp164Zly5bj0uVLdFMhZs6cSXeEwtfXB16eXggPD4e3tzeIFdhsNjn6wVCut1oB5N1qtSEpKQm5jx/B398fvXr2QOPGjdG+fXt06tQJISEhaNWqFWJiYzUeQkKCkZmZiYyMDP1t/Pjx+Pp3X6Nrt66YMmUKPv30N9i4cSMFLBLydQbx5XqrFcBBE+fl5SEiIgKxcTMxbNhwfPvtH7Fy5UrExcVh4aKFWLFiBbZv347hw4ejrOwtfj9xIq5fv04cbEV5eRmGDhuG4tfFMNFia9aswfr18UhJScGdO7ch9IF3eou9nfNaAcrKyvDzzychPi8tLaXfS7Ft2zY94uPjERsTi/nz54N5A9OnTsO+v+xDzsOH1K4Q165doyApIBcUMGrupqcjjePO7dsoKS5GQkICampqwA2olQEiBFArwMuXL/HTT0exevVqzJr1Jw2gywSg+Fj8KFq9ePECt27dwqmEU7hKplarFXa7Q4Nyx44dGgdWixXFZBpQrx6ec38No+nixQvIzc2Fvpx89VTx1xDTcKgnT54g614WXr16RSC+0hI77DbYbXYOG3x9fFCPRO12u0a2hRrxHEk4YONaamqqxoKgXvZIWMq8uroKeXkvdK7g5g9ukaXWAiUlJaiqrEJVVZXeZBgmNGveXEeAg6FXWlKKDh066GhQSkExmUApvVdML6FbVFSEBw8eaIuUlZfD08OTilhQTZqSrMRicsB1CvKszQMSbm5ubqRFufjFMBQ+YZaLZnx3ZgQ0aNgQffr0Ra9evdCjRw8MGjQI7T/7DHJGiFYzN4jGYhmxgDD19fOFjW6y2qw6WsrKymWry/saEai1QFjrMAQGBcLH2wdmkxuGDR2q88G6detwYP9+fPXVEJw9ewbBwcFYy3S8d+9eHD16FIGBQVBKacs0aNAAzVu00FYT0Pn6+oIftVLPnz9HSUmxnqPOZWhxqPHHLT/GgvkLEREZgTZtwrRfBXBeXt4Ibd0aAwcOJODsKCktQcr1FPgxT7QgM09PD01OLBEZGYnu3bujfkB9iLkFG4Id2SDAlDCXOchPP/lj6BdaXQiMGjUSe3bv1rH/9NkzyBBDGcoAKxekIE2dMhXjosfBne4SLaUekA5q6ILk5CQcO3YMhUWFXHKgiCldXEcD4e3bt7h79662QN20b3Bn7W1iLg8JDUWTJk3gR/N99910eHiIhg7Ur18fQ4YMwYABA9CQeJBDAjrRTKKBOpA4xWVC0+9ceMN84uHpyRphpoA1uHHjBuzyXQ67xgcCyJpSCoLo4OAQ1PPzZ2RUcvmdzRQUraG4IkxycnK0ZnzVt9QMGUrJDmhmlayQbmY32FhnfmUOKWcG1ZtdPy4BnAdcayhgLnBzd9NVTRkG7MwHTppUi6CRX0H6XdYA+QbXcVmTwiPCGTzn7ubOFF0Ow2RwiwOPHz1CVtZ9WkooOLkZzsf7BVpIZ7BMEr969Sp27dqlS68AyrnX+Sshl6F9qiAY8fLygp1aCnNyg9nNDV7eXpDwFNe6u3tAhLty5bJ+Qi4FuARQ8qqHaCRofcg8L8Ds0qULmjRtwm/v9/CFmlUg9/FjTh346KNmYMvGufMWkJkMg7436azpYLr28fGFhLAoJUlK76TeWgCr1YKCggJtmuqaakjMCg6SLyRr9DdnRjScPtDn5KeiohwFRLlo3LdvH3RkFVUOp5CkC8GCRIlobXPY+G5Cj549Ibgpfv0aYiWllFhAITU1TTcOdC/K3pZpASSOE04lsD9I1ulZTCmdkQzxdQXBVc50K4zc2aSkpFyHYTgFMAyDcwOyR/ZKgVJKIZypXCqthHcxy7aDDLUFbt++hXv37mkfPmP8S9UDLyEwZ85sxMbGIi4uDuMnjMekSd/ofC8CWmg5YWYyTBjO/sGbBYvHtOklP4j2YqGaGgskHL2ZZdk5o5DWTkqmYqw9rIZS3ay67q9bF48lS5Zo/0rbpZTCY/p5N5PTzp07kZiQCOkZpGXLz39JKyp4Ms4FJ5IPRCgRQJ7CVObO4YA/W7v09DRioka7+v79+7ps0wIKAfUDIMln1ao/I+NuBiawtRLUN2rUSJtStBBTislEK+mWRdhQJi1x2Zy5c3QfIVpTKs0ANK+TOaCUgoDw1MmTungppZB97z4rpRUG5/RNOCSvT2SLFRMbg8ysDLxicxrKaiiMUefyZLiFtWmD6HHRWL58OSI7RRIjlaiorAA5QS6lFKfvh6ylpaUilz2HFKzs7GydY/z9fUELAG3btsFU9vr5ZLphwwakpaXrlGtnAnLIaddQSiGQTensWbOQkZnBjFkEqYq9e/eB1P4WzZuhaZOmOm0HBQXp/iGE1dNkMun/EKJM23btcJKWGDJkMAICApwCCJC6fPEF9uzZg8WLF2vQNWXs5zzKQQuGoFJKiyCuyM3NxZOnT1kx29CE1fqPSCT7heXLlqEP+wUBbHLyBSQnJesGdhaF/S0rqaeXJ8SldrboXdnSiwUNQzkFAC+lFCScBg8ezI5XQvEFFsxfoLXhZ8ApA0Fk1RXzNhvOLEaOAHDsmDHIZic0YsQIHS2ff94eQcFBupWXTjkiMpLWCEdgYCBGjx6Npd9/Dy8fb4BEGQUOiGnejWbNmtG3y7Bv337dA2Y/yIZcIpyEWVhYa4SHh/M/4Ej8YfJknVYPHTqkmX056EuthOwXa8kQcEezq4roGI54RpnMpbckUjVfIysrC5cuX8blOuPa1X/gxIm/YRX/nlmqLToSOnfqjEhmOx9KHhUVxVAtx48/rsGFCxfYxOajVcuW+OX6L046l97TE7qSW7p27YZidkRXWV9kTYaA8d8AAAD//0ETEQ4AAAAGSURBVAMAkxZvMbKHjbQAAAAASUVORK5CYII=',
114
+ };
115
+ /** Anzeigename je Harness-Slug (Slugs bleiben in Daten, Schlüsseln und IPC). */
116
+ var HARNESS_NAME = { 'claude-code': 'Claude Code', hermes: 'Hermes', agents: 'Agents' };
117
+ function harnessName(h) { return HARNESS_NAME[h] || h; }
118
+ function harnessZeichen(h) {
119
+ if (HARNESS_BILD[h]) {
120
+ var img = document.createElement('img');
121
+ img.src = HARNESS_BILD[h];
122
+ img.className = 'hsymbol';
123
+ img.alt = '';
124
+ img.setAttribute('aria-hidden', 'true');
125
+ return img;
126
+ }
127
+ var z = HARNESS_ZEICHEN[h];
128
+ var svg = document.createElementNS('http://www.w3.org/2000/svg', 'svg');
129
+ svg.setAttribute('viewBox', z ? z.viewBox : '0 0 16 16');
130
+ svg.setAttribute('fill', 'currentColor');
131
+ svg.setAttribute('class', 'hsymbol');
132
+ svg.setAttribute('aria-hidden', 'true');
133
+ svg.innerHTML = z ? z.inhalt : '';
134
+ return svg;
135
+ }
136
+ function beschriftet(el, h) {
137
+ el.insertBefore(harnessZeichen(h), el.firstChild);
138
+ return el;
139
+ }
140
+
141
+ /** Kleine Harness-Zeichen einer Zeile (E53), ein Zeichen je genannten Harness, mit
142
+ Sammel-aria-label statt einzelner Beschriftung je Icon. */
143
+ function harnesseZeichenSammel(harnesse) {
144
+ var wrap = e('span', 'zeilenzeichen');
145
+ wrap.setAttribute('role', 'img');
146
+ wrap.setAttribute('aria-label', harnesse.map(harnessName).join(', '));
147
+ harnesse.forEach(function (h) { wrap.appendChild(harnessZeichen(h)); });
148
+ return wrap;
149
+ }
150
+
151
+ /** Ordner-Symbol vom „Verzeichnis hinzufügen"-Knopf, hier für den karten-weiten
152
+ „Skills hinzufügen"-Knopf im Chip „Alle" (E53 Pkt. 4). */
153
+ function ordnerZeichen() {
154
+ var svg = document.createElementNS('http://www.w3.org/2000/svg', 'svg');
155
+ svg.setAttribute('viewBox', '0 0 16 16');
156
+ svg.setAttribute('fill', 'none');
157
+ svg.setAttribute('stroke', 'currentColor');
158
+ svg.setAttribute('stroke-width', '1.5');
159
+ svg.setAttribute('aria-hidden', 'true');
160
+ svg.innerHTML = '<path d="M1.2 13.2V4.1h3.9l1.3 1.8h5.2v7.3z"/><path d="M13.2 1.9v4.6M10.9 4.2h4.6"/>';
161
+ return svg;
162
+ }
163
+
164
+ /* ---------------- Ort- und Grund-Texte ---------------- */
165
+
166
+ function ortName(ort) {
167
+ if (ort.art === 'home') return 'Home';
168
+ if (ort.art === 'profil') return 'Profil „' + ort.name + '“';
169
+ return 'Arbeitsverzeichnis ' + pfadKurz(ort.pfad);
170
+ }
171
+ function zielTitel(z) { return harnessName(z.ziel.harness) + ' · ' + ortName(z.ziel.ort); }
172
+ function zielKey(ziel) {
173
+ var o = ziel.ort;
174
+ var ort = o.art === 'home' ? 'home' : o.art === 'profil' ? 'profil:' + o.name : 'dir:' + o.pfad;
175
+ return ziel.harness + '/' + ort;
176
+ }
177
+ function quelleKurz(quelle) { return quelle.art === 'git' ? quelle.url : quelle.pfad; }
178
+ /**
179
+ * Adresse einer Quelle als Text, egal woher sie kommt: aus dem Ist-Zustand trägt sie `url` bzw.
180
+ * `pfad`, aus `hmgr:quellen` (Einstellungen) nur das schon fertige Feld `text`. Beides ist
181
+ * dasselbe (`quelleAlsText`), aber nur so findet auch die Einstellungen-Liste ihr Icon.
182
+ */
183
+ function quelleAdresse(quelle) { return quelle.text != null ? quelle.text : quelleKurz(quelle); }
184
+ /**
185
+ * Letztes nicht leeres Segment eines Pfads oder einer URL, ohne abschließende Schrägstriche.
186
+ * Trenner sind `/` und `\`, wie in `letztesSegment` im Kern (E65/B8, dort führend).
187
+ */
188
+ function letztesSegment(text) {
189
+ var bereinigt = text.replace(/[/\\]+$/, '');
190
+ var segmente = bereinigt.split(/[/\\]/).filter(function (s) { return s.length > 0; });
191
+ return segmente.length > 0 ? segmente[segmente.length - 1] : bereinigt;
192
+ }
193
+ /**
194
+ * Anzeigename einer Quelle (E59): der gespeicherte Name (Feld `quelle.name`, kommt über IPC mit,
195
+ * wo der Main-Prozess ihn nicht schon direkt liefert), sonst abgeleitet wie im Kern
196
+ * (`quelleName`/`quelleNameAbleiten` in modell.ts, dort führend): letztes Pfadsegment der URL
197
+ * ohne `.git`-Suffix, bei einer lokalen Quelle der Ordnername. Kleine Kopie, weil der
198
+ * Renderer-Prozess den Kern nicht importieren kann (kein Node-Zugriff, contextIsolation).
199
+ */
200
+ function quelleAnzeigename(quelle) {
201
+ if (quelle.name) return quelle.name;
202
+ if (quelle.art === 'lokal') return letztesSegment(quelle.pfad);
203
+ var ohneSchraeg = quelle.url.replace(/\/+$/, '');
204
+ var ohneGit = ohneSchraeg.endsWith('.git') ? ohneSchraeg.slice(0, -4) : ohneSchraeg;
205
+ return letztesSegment(ohneGit);
206
+ }
207
+ /** Ein `<span>` mit dem Anzeigenamen der Quelle, die volle Adresse als title-Tooltip (E59). */
208
+ function quelleSpan(klasse, quelle, rest, praefix) {
209
+ var el = e('span', klasse, (praefix || '') + quelleAnzeigename(quelle) + (rest || ''));
210
+ el.title = quelleKurz(quelle);
211
+ return el;
212
+ }
213
+ /* ---------------- Quellen: Icon und Sprung nach draussen (E60) ---------------- */
214
+
215
+ /** Das kleine Pfeilsymbol hinter einem Quellennamen: „das geht nach draussen". */
216
+ function externSvg() {
217
+ var svg = document.createElementNS('http://www.w3.org/2000/svg', 'svg');
218
+ svg.setAttribute('viewBox', '0 0 12 12');
219
+ svg.setAttribute('class', 'extern');
220
+ svg.setAttribute('fill', 'none');
221
+ svg.setAttribute('stroke', 'currentColor');
222
+ svg.setAttribute('stroke-width', '1.6');
223
+ svg.setAttribute('aria-hidden', 'true');
224
+ svg.innerHTML = '<path d="M3.6 8.4 8.4 3.6"/><path d="M4.9 3.6h3.5v3.5"/>';
225
+ return svg;
226
+ }
227
+
228
+ /** Generisches Ordner-/Repo-Symbol (E60 F35): lokale Quellen und jeder Icon-Fehlschlag. */
229
+ function repoSvgGenerisch() {
230
+ var svg = document.createElementNS('http://www.w3.org/2000/svg', 'svg');
231
+ svg.setAttribute('viewBox', '0 0 20 20');
232
+ svg.setAttribute('aria-hidden', 'true');
233
+ svg.innerHTML = '<rect width="20" height="20" rx="5" fill="#FFFFFF"/>'
234
+ + '<path d="M4.2 14.3V5.4h4.1l1.3 1.8h6.2v7.1z" fill="none" stroke="#4E5B66" stroke-width="1.5" stroke-linejoin="round"/>';
235
+ return svg;
236
+ }
237
+
238
+ /** Icon-Cache im Renderer, Schlüssel wie quelleKurz: dataURL nach Erfolg, null nach Fehlschlag
239
+ oder bei einer lokalen Quelle, sonst ein noch laufendes Promise. Verhindert doppelte
240
+ IPC-Aufrufe für dieselbe Quelle, wenn sie in mehreren Zeilen auftaucht. */
241
+ var ICON_CACHE = {};
242
+ function ladeIcon(quelleText) {
243
+ var vorhanden = ICON_CACHE[quelleText];
244
+ if (vorhanden !== undefined) return vorhanden instanceof Promise ? vorhanden : Promise.resolve(vorhanden);
245
+ var p = window.hmgr.icon(quelleText).then(function (ergebnis) {
246
+ var url = ergebnis && ergebnis.ok ? ergebnis.daten : null;
247
+ ICON_CACHE[quelleText] = url;
248
+ return url;
249
+ }).catch(function () {
250
+ ICON_CACHE[quelleText] = null;
251
+ return null;
252
+ });
253
+ ICON_CACHE[quelleText] = p;
254
+ return p;
255
+ }
256
+
257
+ /** Das 20px-Icon einer Quelle (E60): erst generisch, tauscht asynchron, sobald geladen. Blockiert
258
+ nie den Aufbau der Liste. Lokale Quellen fragen gar nicht erst ab. */
259
+ function repoIcon(quelle) {
260
+ var span = e('span', 'repo');
261
+ span.appendChild(repoSvgGenerisch());
262
+ if (quelle.art === 'lokal') return span;
263
+ ladeIcon(quelleAdresse(quelle)).then(function (url) {
264
+ if (!url || !span.isConnected) return;
265
+ span.textContent = '';
266
+ var img = document.createElement('img');
267
+ img.src = url;
268
+ img.alt = '';
269
+ img.width = 20;
270
+ img.height = 20;
271
+ img.setAttribute('aria-hidden', 'true');
272
+ span.appendChild(img);
273
+ });
274
+ return span;
275
+ }
276
+
277
+ /**
278
+ * Eine Quelle, angeschrieben mit ihrem Namen und anklickbar (E60): keine Unterstreichung, das
279
+ * kleine Pfeilsymbol dahinter trägt das Signalorange. Klick öffnet über den Main-Prozess die
280
+ * Repo-Seite im Browser oder den Ordner im Dateimanager (`window.hmgr.quelleOeffnen`), still im
281
+ * Fehlerfall statt in der Meldungsleiste, ein Öffnen-Fehlschlag soll die Ansicht nicht stören.
282
+ */
283
+ function quelleLinkElement(quelle) {
284
+ var lokal = quelle.art === 'lokal';
285
+ var el = document.createElement(lokal ? 'button' : 'a');
286
+ if (lokal) el.type = 'button'; else el.href = '#';
287
+ el.className = 'qlink';
288
+ el.title = quelleAdresse(quelle);
289
+ // „Dateimanager" statt „Finder" (E68): die GUI läuft auch unter Windows, wo das Ding Explorer
290
+ // heißt, und unter Linux gibt es weder das eine noch das andere.
291
+ el.setAttribute('aria-label', quelleAnzeigename(quelle) + ' ' + (lokal ? 'im Dateimanager öffnen' : 'im Browser öffnen'));
292
+ el.appendChild(document.createTextNode(quelleAnzeigename(quelle)));
293
+ el.appendChild(externSvg());
294
+ el.addEventListener('click', function (ev) {
295
+ ev.preventDefault();
296
+ window.hmgr.quelleOeffnen(quelleAdresse(quelle)).catch(function () {});
297
+ });
298
+ return el;
299
+ }
300
+
301
+ /* ---------------- Zustand (im Renderer gehalten) ---------------- */
302
+
303
+ var ZIELE = [];
304
+ var SKILLS = [];
305
+ var MARKETPLACES = [];
306
+ var QUELLEN = [];
307
+ var HARNESSE_ALLE = [];
308
+ var AKTIVE_HARNESSE = [];
309
+ var HARNESSE_NICHT_INSTALLIERT = [];
310
+ // MCP-Katalog (E61-Folgerunde): je Eintrag { eintrag, eingehaengt, fehlendeVariablen }, dazu
311
+ // Einträge in Harness-Konfigurationen, die nicht im Katalog stehen (nur genannt). Seit F46 nur
312
+ // noch Grundlage für den MCP-Abschnitt der Ziel-Karten (mcpZeilenFuerZiel), die MCP-Ansicht selbst
313
+ // zeigt keine Fundorte mehr.
314
+ var MCP_KATALOG = [];
315
+ var MCP_FREMD = [];
316
+ // Nicht-Katalog-Einträge aus geteilten, nur lesbaren Fundorten (E61-Nachtrag), etwa der
317
+ // .mcp.json einer Projektwurzel.
318
+ var MCP_FREMD_GETEILT = [];
319
+ // Alle Orte, an denen grundsätzlich ein MCP-Server hängen könnte (E61-GUI-Nachtrag), roh aus dem
320
+ // Kern (F42, E16): Grundlage für den MCP-Abschnitt und den „MCP hinzufügen …"-Knopf an den
321
+ // Ziel-Karten — nie am Harness-Namen entschieden, sondern an dieser Liste.
322
+ var MCP_PLAETZE = [];
323
+ var quellenFilter = 'alle';
324
+ var zielFilter = 'alle';
325
+ // Standard-Ref je Quelle (E60), aus dem Katalog abgeleitet (SKILLS trägt ihn schon je Skill): ein
326
+ // Ref an einem Link erscheint nur, wenn er davon abweicht (`@v2.1`). Schlüssel wie quelleKurz.
327
+ var STANDARD_REF = {};
328
+
329
+ function harnessAktiv(h) { return AKTIVE_HARNESSE.indexOf(h) > -1; }
330
+
331
+ // Ignorierte fremde Einträge zählen hier nicht mit (E43): Ignorieren blendet die Warnung aus,
332
+ // sonst bliebe das Ziel im Filter „braucht Aufmerksamkeit" stehen.
333
+ function zielBraucht(z) {
334
+ var ignoriert = z.ignoriert || [];
335
+ return z.eintraege.some(function (ei) {
336
+ if (ei.art === 'fremd') return ignoriert.indexOf(ei.name) === -1;
337
+ return ei.art === 'link-ohne-quelle';
338
+ });
339
+ }
340
+
341
+ // E45: ein Ziel eines inaktiven Harness verschwindet, sobald dort nichts liegt, weder eigene noch
342
+ // fremde Einträge noch eine harness-eigene Registrierung. Ein aktiver Harness zeigt sein Ziel immer.
343
+ function zielSichtbar(z) {
344
+ if (harnessAktiv(z.ziel.harness)) return true;
345
+ return z.eintraege.length > 0 || z.istRegistriert;
346
+ }
347
+
348
+ // Von Hand aufgeklappte Ziele bleiben über Neuaufbauten der Liste offen (E45), Schlüssel wie
349
+ // zielKey. Verzeichnis-Karten (E49) nutzen stattdessen verzKey (reiner Pfad, kollidiert nicht mit
350
+ // zielKey, der immer mit "harness/" beginnt). Nicht persistiert, nur für die Dauer der Sitzung.
351
+ var ZIEL_OFFEN = {};
352
+ function verzKey(pfad) { return 'dir:' + pfad; }
353
+
354
+ // Aktiver Harness-Chip je Verzeichnis-Karte (E53), gleicher Schlüssel wie ZIEL_OFFEN
355
+ // (verzKey(pfad)); fehlt der Eintrag oder zeigt er auf einen nicht mehr beteiligten Harness,
356
+ // gilt „alle". Nicht persistiert, nur für die Dauer der Sitzung.
357
+ var VERZ_CHIP = {};
358
+
359
+ // Aufgeklappte Skill-Details im Katalog (F10), Schlüssel Quelle + Pfad: der Index in SKILLS
360
+ // verschiebt sich, wenn ladeAlles Quellen oder Skills anders sortiert oder eine wegfällt.
361
+ // Nicht persistiert, nur für die Dauer der Sitzung.
362
+ var SKILL_OFFEN = {};
363
+ function skillKey(skill) { return skill.quelleSchluessel + '|' + skill.pfad; }
364
+ // Ein Chip-Klick baut die ganze Liste neu und zerstört dabei den geklickten Knopf. Damit die
365
+ // Tastatur-Bedienung nicht am Seitenanfang landet, merkt sich der Klick, welcher Chip nach dem
366
+ // Neuaufbau den Fokus bekommt.
367
+ var CHIP_FOKUS = null;
368
+ /** Schlüssel der Karte, in der ein Ziel steckt: Arbeitsverzeichnisse sind gebündelt (E49). */
369
+ function kartenKey(ziel) {
370
+ return ziel.ort.art === 'arbeitsverzeichnis' ? verzKey(ziel.ort.pfad) : zielKey(ziel);
371
+ }
372
+
373
+ // Laufende Nummer für die ids der Körper: aus dem Schlüssel abgeleitete ids kollidieren, sobald
374
+ // sich zwei Verzeichnisse nur in Zeichen unterscheiden, die eine id nicht tragen kann.
375
+ var zielKoerperNr = 0;
376
+
377
+ /* ---------------- Laden ---------------- */
378
+
379
+ /**
380
+ * Home-Verzeichnis und Plattform einmal beim Start holen (nur für die Anzeige, siehe oben).
381
+ * Danach stehen die fest formulierten Pfade im Bild in der Schreibweise der Plattform (E68).
382
+ */
383
+ async function ladeUmgebung() {
384
+ var werte = await Promise.all([
385
+ rufe(function () { return window.hmgr.home(); }),
386
+ rufe(function () { return window.hmgr.plattform(); }),
387
+ ]);
388
+ HOME = werte[0];
389
+ TRENNER = plattformTrenner(werte[1]);
390
+ $('mcpkatalogpfad').textContent = pfadSchreibweise('~/.hmgr/mcp.yaml');
391
+ }
392
+
393
+ async function ladeAlles() {
394
+ var version = await rufe(function () { return window.hmgr.version(); });
395
+ $('version').textContent = version;
396
+
397
+ var ergebnisse = await Promise.all([
398
+ rufe(function () { return window.hmgr.istZustand(); }),
399
+ rufe(function () { return window.hmgr.skills(); }),
400
+ rufe(function () { return window.hmgr.marketplaces(); }),
401
+ rufe(function () { return window.hmgr.quellen(); }),
402
+ rufe(function () { return window.hmgr.harnessTypen(); }),
403
+ rufe(function () { return window.hmgr.aktiveHarnesses(); }),
404
+ rufe(function () { return window.hmgr.nichtVorhandeneHarnesses(); }),
405
+ rufe(function () { return window.hmgr.mcpUebersicht(); }),
406
+ rufe(function () { return window.hmgr.mcpPlaetze(); }),
407
+ ]);
408
+ ZIELE = ergebnisse[0];
409
+ SKILLS = ergebnisse[1];
410
+ // Eine alphabetische Liste über alle Quellen; wer nach Quelle sucht, hat die Filter-Chips.
411
+ SKILLS.sort(function (a, b) { return a.name.localeCompare(b.name, 'de') || a.quelleText.localeCompare(b.quelleText, 'de'); });
412
+ MARKETPLACES = ergebnisse[2];
413
+ QUELLEN = ergebnisse[3];
414
+ HARNESSE_ALLE = ergebnisse[4];
415
+ AKTIVE_HARNESSE = ergebnisse[5];
416
+ HARNESSE_NICHT_INSTALLIERT = ergebnisse[6];
417
+ MCP_KATALOG = ergebnisse[7].katalog;
418
+ MCP_FREMD = ergebnisse[7].unbekannt;
419
+ MCP_FREMD_GETEILT = ergebnisse[7].unbekannteGeteilteFundorte;
420
+ MCP_PLAETZE = ergebnisse[8];
421
+ STANDARD_REF = {};
422
+ SKILLS.forEach(function (s) { if (STANDARD_REF[s.quelleText] === undefined) STANDARD_REF[s.quelleText] = s.ref; });
423
+
424
+ baueZiele();
425
+ baueSkills();
426
+ baueQuellenfilter();
427
+ baueMarketplaces();
428
+ baueEinstellungen();
429
+ baueMcp();
430
+ aktualisiereKopf();
431
+ }
432
+
433
+ /** Karten, wie die Zielliste sie zeigt: je Agent-Ziel eine, je Arbeitsverzeichnis eine gebündelte
434
+ (E49). Grundlage für "N von M Karten gezeigt" und den Zähler am Achtung-Chip. */
435
+ function zielKarten() {
436
+ var sichtbar = ZIELE.filter(zielSichtbar);
437
+ var agents = sichtbar.filter(function (z) { return z.ziel.ort.art !== 'arbeitsverzeichnis'; });
438
+ var gruppen = {};
439
+ sichtbar.forEach(function (z) {
440
+ if (z.ziel.ort.art !== 'arbeitsverzeichnis') return;
441
+ var pfad = z.ziel.ort.pfad;
442
+ (gruppen[pfad] = gruppen[pfad] || []).push(z);
443
+ });
444
+ var verzeichnisse = Object.keys(gruppen).map(function (pfad) { return { pfad: pfad, teile: gruppen[pfad] }; });
445
+ return { agents: agents, verzeichnisse: verzeichnisse };
446
+ }
447
+
448
+ // Kopfzahl und Nav-Zahl tragen beide das Wort "Ziele" und stehen gleichzeitig im Bild, also zählen
449
+ // beide im Kern-Sinn: jedes Harness+Ort-Paar einzeln, unabhängig von der Bündelung. Karten zählt
450
+ // nur, was sie auch so nennt: "N von M Karten gezeigt" und der Achtung-Chip (E49).
451
+ function aktualisiereKopf() {
452
+ var sichtbar = ZIELE.filter(zielSichtbar);
453
+ var linkZahl = 0, fremdZahl = 0;
454
+ sichtbar.forEach(function (z) {
455
+ var ignoriert = z.ignoriert || [];
456
+ z.eintraege.forEach(function (ei) {
457
+ if (ei.art === 'fremd') { if (ignoriert.indexOf(ei.name) === -1) fremdZahl++; }
458
+ else linkZahl++;
459
+ });
460
+ });
461
+ $('kopfzahlen').textContent = zahlwort(sichtbar.length, 'Ziel', 'Ziele') + ' · ' +
462
+ zahlwort(linkZahl, 'Link', 'Links') + ' · ' +
463
+ zahlwort(fremdZahl, 'nicht von hmgr verwalteter Eintrag', 'nicht von hmgr verwaltete Einträge');
464
+ $('navzahlziele').textContent = sichtbar.length;
465
+ $('navzahlskills').textContent = SKILLS.length;
466
+ var pluginZahl = MARKETPLACES.reduce(function (n, m) { return n + m.plugins.length; }, 0);
467
+ $('navzahlplugins').textContent = pluginZahl;
468
+
469
+ var karten = zielKarten();
470
+ // Betroffene Karten für den Chip-Zähler: eine Verzeichnis-Karte zählt, wenn mindestens ein
471
+ // Abschnitt (Harness) darin zielBraucht ist.
472
+ var betroffen = karten.agents.filter(zielBraucht).length
473
+ + karten.verzeichnisse.filter(function (g) { return g.teile.some(zielBraucht); }).length;
474
+ var achtungChip = document.querySelector('#ansicht-ziele .chip[data-zielfilter="achtung"]');
475
+ if (achtungChip) achtungChip.textContent = 'Benötigt Aufmerksamkeit (' + betroffen + ')';
476
+ }
477
+
478
+ /* ---------------- Navigation ---------------- */
479
+
480
+ var ANSICHTEN = ['ziele', 'katalog', 'update', 'plugins', 'mcp'];
481
+ function zeigeAnsicht(name) {
482
+ document.querySelectorAll('.plakette').forEach(function (a) {
483
+ if (a.dataset.ansicht === name) a.setAttribute('aria-current', 'page');
484
+ else a.removeAttribute('aria-current');
485
+ });
486
+ ANSICHTEN.forEach(function (x) { $('ansicht-' + x).hidden = x !== name; });
487
+ // Der Bericht ist jetzt sichtbar, die Ankündigung „mit Neuem" hat ihren Dienst getan.
488
+ if (name === 'update' && standZeit !== null) standRuhig();
489
+ window.scrollTo(0, 0);
490
+ }
491
+ document.querySelectorAll('[data-ansicht]').forEach(function (k) {
492
+ k.addEventListener('click', function () { zeigeAnsicht(k.dataset.ansicht); });
493
+ });
494
+
495
+ /* ================= ZIELE ================= */
496
+
497
+ /** Hinweiszeile, wenn ein inaktiver Harness noch Links oder eine eigene Registrierung an diesem
498
+ Ziel hat (E38 R7): Säubern-Weg direkt in der Karte. Von linkTeil und alleTeil geteilt. */
499
+ function ruheZeile(z, hatEigene) {
500
+ var rz = e('div', 'ruhezeile');
501
+ var rsvg = document.createElementNS('http://www.w3.org/2000/svg', 'svg');
502
+ rsvg.setAttribute('viewBox', '0 0 16 16');
503
+ rsvg.setAttribute('stroke-width', '1.6');
504
+ rsvg.setAttribute('aria-hidden', 'true');
505
+ rsvg.innerHTML = '<circle cx="8" cy="8" r="6.5"/><path d="M8 4.5v4M8 10.5v.5"/>';
506
+ rz.appendChild(rsvg);
507
+ rz.appendChild(e('p', null, harnessName(z.ziel.harness) + ' ist inaktiv. '
508
+ + (hatEigene ? 'hmgr pflegt diese Links nicht mehr.' : 'Ein harness-eigener Eintrag verweist noch auf dieses Ziel.')));
509
+ var rk = e('button', 'knopf leise klein', 'Säubern');
510
+ rk.type = 'button';
511
+ rk.setAttribute('aria-label', harnessName(z.ziel.harness) + ' säubern');
512
+ rk.addEventListener('click', function () { frageSaeubern(z.ziel.harness); });
513
+ rz.appendChild(rk);
514
+ return rz;
515
+ }
516
+
517
+ /** Eine Zeile im Fremdkasten für einen fremden Eintrag x an einem Ziel z. Mit mitZeichen=true
518
+ steht davor das kleine Harness-Zeichen seines Fundorts (E53 Pkt. 5, „Alle"-Ansicht). Gibt
519
+ neben dem li auch zurück, ob eine Quelle erkannt wurde (für den Erklärtext). */
520
+ function fremdZeile(x, z, mitZeichen) {
521
+ var prognosen = z.uebernahme || {};
522
+ var prognose = prognosen[x.name];
523
+ var erkannt = prognose && prognose.art === 'erkannt';
524
+ var li = e('li');
525
+ var kopf = e('span', 'fremdkopf');
526
+ // Nur die „Alle"-Ansicht braucht die Zeile als Flex-Kasten neben dem Harness-Zeichen. Unter
527
+ // einem Harness-Chip bleibt es der schlichte Span von vorher: ein Flex-Kasten bricht lange
528
+ // Namen anders um als der Fließtext.
529
+ var namezeile = e('span', mitZeichen ? 'fremdname' : null);
530
+ if (mitZeichen) namezeile.appendChild(harnesseZeichenSammel([z.ziel.harness]));
531
+ namezeile.appendChild(document.createTextNode(x.name));
532
+ kopf.appendChild(namezeile);
533
+ if (erkannt) kopf.appendChild(quelleSpan('mono', prognose.quelle, ' @ ' + prognose.ref));
534
+ li.appendChild(kopf);
535
+ var knoepfe = e('span', 'fremdknoepfe');
536
+ if (erkannt) {
537
+ var ue = e('button', 'knopf leise klein', 'Übernehmen');
538
+ ue.type = 'button';
539
+ ue.setAttribute('aria-label', x.name + ' übernehmen');
540
+ ue.addEventListener('click', function () { oeffneUebernahme(z.ziel, x.name); });
541
+ knoepfe.appendChild(ue);
542
+ }
543
+ var ig = e('button', 'knopf leise klein', 'Ignorieren');
544
+ ig.type = 'button';
545
+ ig.setAttribute('aria-label', x.name + ' ignorieren');
546
+ ig.addEventListener('click', async function () {
547
+ await rufe(function () {
548
+ return window.hmgr.eintragIgnorieren({ harness: z.ziel.harness, ort: z.ziel.ort, name: x.name });
549
+ });
550
+ await ladeAlles();
551
+ });
552
+ knoepfe.appendChild(ig);
553
+ var lo = e('button', 'knopf leise klein', 'Löschen');
554
+ lo.type = 'button';
555
+ lo.setAttribute('aria-label', x.name + ' löschen');
556
+ lo.addEventListener('click', function () { frageLoeschen(x.name, z.ziel, z.verzeichnis); });
557
+ knoepfe.appendChild(lo);
558
+ li.appendChild(knoepfe);
559
+ // Ohne erkannte Quelle: Ursache und Handreichungen direkt im Kasten statt eines Vorschau-Dialogs (E43, E48).
560
+ if (!erkannt) {
561
+ var grundInfo = UEBERNAHME_GRUENDE[prognose ? prognose.grund : 'kein-link'];
562
+ if (grundInfo) {
563
+ var ursache = e('div', 'fremdursache');
564
+ ursache.appendChild(e('p', 'mono', grundInfo.ursache));
565
+ if (grundInfo.tipps && grundInfo.tipps.length > 0) {
566
+ var tippbox = e('div', 'tipps');
567
+ tippbox.appendChild(e('p', 'tippkopf', 'Außerhalb von hmgr:'));
568
+ var tul = e('ul', 'tippliste');
569
+ grundInfo.tipps.forEach(function (t) { tul.appendChild(e('li', null, '→ ' + t)); });
570
+ tippbox.appendChild(tul);
571
+ ursache.appendChild(tippbox);
572
+ }
573
+ li.appendChild(ursache);
574
+ }
575
+ }
576
+ return { li: li, erkannt: erkannt };
577
+ }
578
+
579
+ /** Fremdkasten über eine Liste von {x, z}-Paaren (ein Ziel bei linkTeil, mehrere bei alleTeil). */
580
+ function fremdKasten(paare, mitZeichen) {
581
+ var kasten = e('div', 'fremdkasten');
582
+ var text = e('div', 'fremdtext');
583
+ var p = e('p');
584
+ p.appendChild(e('strong', null, paare.length + (paare.length === 1
585
+ ? ' nicht von hmgr verwalteter Eintrag'
586
+ : ' nicht von hmgr verwaltete Einträge')));
587
+ text.appendChild(p);
588
+ var namen = e('ul', 'fremdnamen');
589
+ var einErkannt = false;
590
+ paare.forEach(function (paar) {
591
+ var r = fremdZeile(paar.x, paar.z, mitZeichen);
592
+ if (r.erkannt) einErkannt = true;
593
+ namen.appendChild(r.li);
594
+ });
595
+ text.appendChild(namen);
596
+ // Der Erklärtext nennt Übernehmen nur, wo der Knopf auch steht (E48).
597
+ text.appendChild(e('p', 'fremderklaerung',
598
+ (einErkannt ? 'Übernehmen zeigt zuerst eine Vorschau und macht daraus einen Link auf die Quelle. ' : '')
599
+ + (paare.length === 1
600
+ ? 'Ignorieren blendet ihn aus, er bleibt liegen. Löschen entfernt ihn endgültig von der Platte.'
601
+ : 'Ignorieren blendet einen Eintrag aus, er bleibt liegen. Löschen entfernt ihn endgültig von der Platte.')));
602
+ kasten.appendChild(text);
603
+ return kasten;
604
+ }
605
+
606
+ /** Zeile ignorierter fremder Einträge über eine Liste von {x, z}-Paaren, analog fremdKasten. */
607
+ function ignoriertZeile(paare, mitZeichen) {
608
+ var izeile = e('div', 'ignoriertzeile');
609
+ izeile.appendChild(e('span', 'ignoriertzahl', paare.length + ' ignoriert'));
610
+ paare.forEach(function (paar) {
611
+ var x = paar.x, z = paar.z;
612
+ var spanne = e('span', 'ignoriertname');
613
+ if (mitZeichen) spanne.appendChild(harnesseZeichenSammel([z.ziel.harness]));
614
+ spanne.appendChild(e('span', null, x.name));
615
+ var wb = e('button', 'knopf leise klein', 'Wieder beachten');
616
+ wb.type = 'button';
617
+ wb.setAttribute('aria-label', x.name + ' wieder beachten');
618
+ wb.addEventListener('click', async function () {
619
+ await rufe(function () {
620
+ return window.hmgr.eintragBeachten({ harness: z.ziel.harness, ort: z.ziel.ort, name: x.name });
621
+ });
622
+ await ladeAlles();
623
+ });
624
+ spanne.appendChild(wb);
625
+ izeile.appendChild(spanne);
626
+ });
627
+ return izeile;
628
+ }
629
+
630
+ /** Sichtbare Konfiguration eines Ziels mit der Fähigkeit „verwalteter Skill-Ordner": bei Hermes
631
+ der external_dirs-Ordner, zum Festlegen oder Entfernen (E56). Ob ein Ziel das kann, sagt der
632
+ Kern mit kannSkillOrdner; der Harness-Name ist hier kein Kriterium (E16). */
633
+ function konfigZeile(z) {
634
+ if (!z.kannSkillOrdner) return null;
635
+ var kz = e('div', 'konfigzeile');
636
+ if (z.verwalteterSkillOrdner) {
637
+ kz.appendChild(e('b', null, 'Liest Skills aus'));
638
+ kz.appendChild(e('span', 'mono', pfadKurz(z.verwalteterSkillOrdner)));
639
+ var weg = e('button', 'knopf leise klein', 'Entfernen …');
640
+ weg.type = 'button';
641
+ weg.setAttribute('aria-label', 'Skill-Ordner für ' + zielTitel(z) + ' entfernen');
642
+ weg.addEventListener('click', function () { frageSkillOrdnerEntfernen(z); });
643
+ kz.appendChild(weg);
644
+ } else {
645
+ kz.appendChild(e('span', 'konfigquelle', 'Kein Skill-Ordner verwaltet.'));
646
+ var fest = e('button', 'knopf leise klein', 'Skill-Ordner festlegen …');
647
+ fest.type = 'button';
648
+ fest.setAttribute('aria-label', 'Skill-Ordner für ' + zielTitel(z) + ' festlegen');
649
+ fest.addEventListener('click', function () { oeffneSkillOrdnerFestlegen(z); });
650
+ kz.appendChild(fest);
651
+ }
652
+ return kz;
653
+ }
654
+
655
+ function linkTeil(z) {
656
+ var teil = document.createDocumentFragment();
657
+ var eigene = z.eintraege.filter(function (ei) { return ei.art !== 'fremd'; });
658
+ var fremde = z.eintraege.filter(function (ei) { return ei.art === 'fremd'; });
659
+
660
+ // Harness abgeschaltet, Konfiguration liegt noch (E38, GRILL.md Entwurfskritik 4): der Weg,
661
+ // sie loszuwerden, steht hier oben in der Karte. Zählt auch ohne Links, wenn die harness-eigene
662
+ // Registrierung noch da ist (E38 R7), etwa Hermes' external_dirs-Eintrag ohne Links darunter.
663
+ if (!harnessAktiv(z.ziel.harness) && (eigene.length > 0 || z.istRegistriert)) {
664
+ teil.appendChild(ruheZeile(z, eigene.length > 0));
665
+ }
666
+
667
+ var kz = konfigZeile(z);
668
+ if (kz) teil.appendChild(kz);
669
+
670
+ if (fremde.length > 0) {
671
+ var ignoriertNamen = z.ignoriert || [];
672
+ var beachtete = fremde.filter(function (x) { return ignoriertNamen.indexOf(x.name) === -1; });
673
+ var ignorierte = fremde.filter(function (x) { return ignoriertNamen.indexOf(x.name) !== -1; });
674
+
675
+ if (beachtete.length > 0) {
676
+ teil.appendChild(fremdKasten(beachtete.map(function (x) { return { x: x, z: z }; }), false));
677
+ }
678
+ if (ignorierte.length > 0) {
679
+ teil.appendChild(ignoriertZeile(ignorierte.map(function (x) { return { x: x, z: z }; }), false));
680
+ }
681
+ }
682
+
683
+ var fussContainer;
684
+ if (eigene.length === 0) {
685
+ var leer = e('div', 'leerzeile');
686
+ leer.appendChild(e('p', 'ohne', 'Hier liegt noch nichts.'));
687
+ teil.appendChild(leer);
688
+ fussContainer = leer;
689
+ } else {
690
+ // Fünf Spalten (E60): Icon, Skill, Beschreibung (die breiteste, sie beantwortet die Frage,
691
+ // wegen der die Tabelle gelesen wird), Quelle, Aktion.
692
+ var kopf = e('div', 'linkkopf');
693
+ kopf.appendChild(e('span'));
694
+ kopf.appendChild(e('span', 'etikett', 'Skill'));
695
+ kopf.appendChild(e('span', 'etikett', 'Beschreibung'));
696
+ kopf.appendChild(e('span', 'etikett', 'Quelle'));
697
+ kopf.appendChild(e('span'));
698
+ teil.appendChild(kopf);
699
+
700
+ var ul = e('ul', 'linkliste');
701
+ // Defekte Links (ohne Quelle) zuerst, sie brauchen Aufmerksamkeit (E45), sonst alphabetisch.
702
+ eigene
703
+ .slice()
704
+ .sort(function (a, b) {
705
+ var da = a.art === 'link-ohne-quelle', db = b.art === 'link-ohne-quelle';
706
+ if (da !== db) return da ? -1 : 1;
707
+ var na = a.art === 'link' ? a.link.name : a.name;
708
+ var nb = b.art === 'link' ? b.link.name : b.name;
709
+ return na.localeCompare(nb, 'de');
710
+ })
711
+ .forEach(function (ei) {
712
+ var name = ei.art === 'link' ? ei.link.name : ei.name;
713
+ var li = e('li');
714
+
715
+ if (ei.art === 'link') li.appendChild(repoIcon(ei.link.checkout.quelle));
716
+ else li.appendChild(repoIcon({ art: 'lokal', pfad: ei.checkoutPfad }));
717
+
718
+ li.appendChild(e('span', 'name', name));
719
+
720
+ // Die Beschreibung aus der SKILL.md (E39): die Frage, wegen der die Zeile gelesen wird,
721
+ // deshalb die breiteste Spalte. Ein Link ohne bekannte Quelle kennt seinen Skill nicht.
722
+ li.appendChild(wasZelle(ei.art === 'link' ? ei.link.skill : null));
723
+
724
+ // Quelle: der Name, anklickbar, dahinter nur, was vom Normalfall abweicht — ein Ref, das
725
+ // vom Standard-Ref der Quelle abweicht, und der Grund „über Zeiger" (E57). „Manuell" (der
726
+ // Normalfall) entfällt ersatzlos, ein Plugin-Grund steht stattdessen in der Aktionsspalte.
727
+ var qz = e('span', 'quelle');
728
+ if (ei.art === 'link') {
729
+ var einschraenkung = z.einschraenkungen[name];
730
+ if (einschraenkung) qz.appendChild(document.createTextNode(einschraenkung + ' → '));
731
+ qz.appendChild(quelleLinkElement(ei.link.checkout.quelle));
732
+ var standardRef = STANDARD_REF[quelleKurz(ei.link.checkout.quelle)];
733
+ if (ei.link.checkout.ref && standardRef && ei.link.checkout.ref !== standardRef) {
734
+ qz.appendChild(e('span', 'ref', '@' + ei.link.checkout.ref));
735
+ }
736
+ } else {
737
+ qz.appendChild(e('span', 'mono', pfadKurz(ei.checkoutPfad)));
738
+ }
739
+ var grund = z.gruende[name];
740
+ if (grund && grund.art === 'zeiger') qz.appendChild(e('span', 'grundmarke', 'über Zeiger'));
741
+ li.appendChild(qz);
742
+
743
+ // Aus einem Plugin verlinkt: kein Entfernen-Knopf, das geht nur über das Plugin — der
744
+ // Grund erklärt hier zugleich, warum der Knopf fehlt.
745
+ if (grund && grund.art === 'plugin') {
746
+ li.appendChild(e('span', 'kein-weg', 'nur über Plugin „' + grund.plugin.split('@')[0] + '“'));
747
+ } else {
748
+ var weg = e('button', 'knopf leise klein', 'Entfernen');
749
+ weg.type = 'button';
750
+ weg.setAttribute('aria-label', 'Link auf ' + name + ' entfernen');
751
+ weg.addEventListener('click', function () { frageEntfernen(name, z.ziel); });
752
+ li.appendChild(weg);
753
+ }
754
+ ul.appendChild(li);
755
+ });
756
+ teil.appendChild(ul);
757
+
758
+ // MCP-Abschnitt (E61-GUI-Nachtrag): unter der Skill-Link-Tabelle, vor dem Kartenfuß mit den
759
+ // Knöpfen, Gegenrichtung zur MCP-Ansicht.
760
+ var mcpAbschnMitTabelle = mcpAbschnitt(z.ziel);
761
+ if (mcpAbschnMitTabelle) teil.appendChild(mcpAbschnMitTabelle);
762
+
763
+ var fuss = e('div', 'zielfuss');
764
+ teil.appendChild(fuss);
765
+ fussContainer = fuss;
766
+ }
767
+
768
+ // Ohne eigene Links (leer-Fall) steht der MCP-Abschnitt hinter der Leerzeile, die dort schon den
769
+ // Kartenfuß bildet (Nachricht + Knöpfe in einer Zeile, wie überall sonst im leeren Zustand).
770
+ if (eigene.length === 0) {
771
+ var mcpAbschnOhneTabelle = mcpAbschnitt(z.ziel);
772
+ if (mcpAbschnOhneTabelle) teil.appendChild(mcpAbschnOhneTabelle);
773
+ }
774
+
775
+ fussContainer.appendChild(skillsKnopf(z));
776
+ var mcpKnopf = mcpHinzufuegenKnopf(z.ziel);
777
+ if (mcpKnopf) fussContainer.appendChild(mcpKnopf);
778
+
779
+ return teil;
780
+ }
781
+
782
+ /** Knopf „Skills hinzufügen" im Körper einer Ziel-Karte (E47), Gegenrichtung zu starteVerlinken.
783
+ Reichweite ist immer dieser eine Harness, das Zeichen steht davor (E53 Pkt. 4, 7, 8). */
784
+ function skillsKnopf(z) {
785
+ var knopf = e('button', 'knopf leise klein mitsymbol', 'Skills hinzufügen');
786
+ knopf.type = 'button';
787
+ knopf.insertBefore(harnessZeichen(z.ziel.harness), knopf.firstChild);
788
+ knopf.setAttribute('aria-label', 'Skills zu ' + zielTitel(z) + ' hinzufügen (' + harnessName(z.ziel.harness) + ')');
789
+ knopf.addEventListener('click', function () { oeffneSkillsHinzufuegen(z); });
790
+ return knopf;
791
+ }
792
+
793
+ /** Knopf „Skills hinzufügen" für eine Verzeichnis-Karte im Chip „Alle" (E53 Pkt. 4): Reichweite
794
+ sind alle beteiligten Harnesses, das Ordner-Symbol steht davor. */
795
+ function alleSkillsKnopf(pfad, teile) {
796
+ var knopf = e('button', 'knopf leise klein mitsymbol', 'Skills hinzufügen');
797
+ knopf.type = 'button';
798
+ knopf.insertBefore(ordnerZeichen(), knopf.firstChild);
799
+ knopf.setAttribute('aria-label', 'Skills zu ' + pfadKurz(pfad) + ' hinzufügen (alle Harnesses)');
800
+ knopf.addEventListener('click', function () { oeffneSkillsHinzufuegenAlle(pfad, teile); });
801
+ return knopf;
802
+ }
803
+
804
+ /* ---------------- MCP an der Ziel-Karte (E61-GUI-Nachtrag) ---------------- */
805
+ /* Gegenrichtung zur MCP-Ansicht (E47-Muster): ob ein Harness an einem Ort MCP kann, entscheidet
806
+ nie der Harness-Name (E16), sondern einzig, ob MCP_PLAETZE (roh aus dem Kern, F42) diesen Ort
807
+ nennt. */
808
+ function mcpKannAn(ziel) {
809
+ var zk = zielKey(ziel);
810
+ return MCP_PLAETZE.some(function (p) { return zielKey(p) === zk; });
811
+ }
812
+
813
+ /** Die MCP-Zeilen eines Ziels, aus den drei Quellen der MCP-Ansicht zusammengesucht: eingehängte
814
+ Katalog-Einträge (mit Entfernen-Knopf), ihre Fundorte in der geteilten .mcp.json des Projekts
815
+ (E61-Nachtrag, seit E64 mit Übernehmen-Knopf) und Einträge, die dort hängen, aber nicht im
816
+ Katalog stehen (stiller Vermerk). */
817
+ function mcpZeilenFuerZiel(ziel) {
818
+ var zk = zielKey(ziel);
819
+ var zeilen = [];
820
+ MCP_KATALOG.forEach(function (k) {
821
+ if (k.eingehaengt.some(function (z) { return zielKey(z) === zk; })) {
822
+ zeilen.push({ art: 'katalog', name: k.eintrag.name, beschreibung: k.eintrag.beschreibung, eintrag: k.eintrag });
823
+ }
824
+ k.geteilteFundorte.forEach(function (g) {
825
+ if (zielKey(g.ziel) === zk)
826
+ zeilen.push({ art: 'geteilt', name: k.eintrag.name, beschreibung: k.eintrag.beschreibung, datei: g.datei });
827
+ });
828
+ });
829
+ MCP_FREMD.forEach(function (x) { if (zielKey(x.ziel) === zk) zeilen.push({ art: 'fremd', name: x.name }); });
830
+ // Ein Eintrag aus der geteilten .mcp.json, den der Katalog nicht kennt: „nicht im Katalog" allein
831
+ // läse sich wie eine Einladung, ihn in die mcp.yaml zu schreiben. Wo er wirklich steht, gehört
832
+ // daneben — und übernehmen lässt er sich auch ohne Katalog-Eintrag (E64, der Block kommt aus der
833
+ // Datei).
834
+ MCP_FREMD_GETEILT.forEach(function (x) {
835
+ if (zielKey(x.ziel) === zk) zeilen.push({ art: 'fremd-geteilt', name: x.name, datei: x.datei });
836
+ });
837
+ zeilen.sort(function (a, b) { return a.name.localeCompare(b.name, 'de'); });
838
+ return zeilen;
839
+ }
840
+
841
+ /** Der MCP-Abschnitt unter der Skill-Link-Tabelle einer Ziel-Karte (E61-GUI-Nachtrag): nur, wenn
842
+ dort etwas eingehängt ist oder der „hinzufügen"-Knopf ohnehin sichtbar wäre (mcpKannAn). */
843
+ function mcpAbschnitt(ziel) {
844
+ var zeilen = mcpZeilenFuerZiel(ziel);
845
+ if (zeilen.length === 0 && !mcpKannAn(ziel)) return null;
846
+ var abschnitt = e('div', 'mcpkarteabschnitt');
847
+ abschnitt.appendChild(e('span', 'etikett', 'MCP'));
848
+ if (zeilen.length === 0) {
849
+ abschnitt.appendChild(e('p', 'ohne mcpkarteleer', 'Noch nichts eingehängt.'));
850
+ return abschnitt;
851
+ }
852
+ var ul = e('ul', 'mcpkartezeilen');
853
+ zeilen.forEach(function (row) {
854
+ var li = e('li');
855
+ li.appendChild(e('span', 'name', row.name));
856
+ if (row.art === 'katalog') {
857
+ li.appendChild(e('span', 'was', row.beschreibung || ''));
858
+ var weg = e('button', 'knopf leise klein', 'Entfernen');
859
+ weg.type = 'button';
860
+ weg.setAttribute('aria-label', row.name + ' aus ' + zielTitel({ ziel: ziel }) + ' aushängen');
861
+ weg.addEventListener('click', function () { frageMcpAus(row.eintrag, ziel); });
862
+ li.appendChild(weg);
863
+ } else if (row.art === 'geteilt' || row.art === 'fremd-geteilt') {
864
+ // E64: der Vermerk nennt die Bedeutung („steht im Projekt, nicht bei dir"), nicht mehr bloß
865
+ // den Dateinamen — und daneben steht der Weg, das zu ändern.
866
+ li.appendChild(row.art === 'geteilt'
867
+ ? e('span', 'was', row.beschreibung || '')
868
+ : e('span', 'was fehlt', 'nicht im Katalog'));
869
+ li.appendChild(e('span', 'geteiltvermerk', 'in der .mcp.json des Projekts'));
870
+ var her = e('button', 'knopf leise klein', 'Übernehmen …');
871
+ her.type = 'button';
872
+ her.setAttribute('aria-label', row.name + ' aus der .mcp.json des Projekts in deine private Konfiguration übernehmen');
873
+ her.addEventListener('click', function () { frageMcpUebernahme(row.name, ziel, row.datei); });
874
+ li.appendChild(her);
875
+ } else {
876
+ li.appendChild(e('span', 'was fehlt', 'nicht im Katalog'));
877
+ li.appendChild(e('span'));
878
+ }
879
+ ul.appendChild(li);
880
+ });
881
+ abschnitt.appendChild(ul);
882
+ return abschnitt;
883
+ }
884
+
885
+ /** Knopf „MCP hinzufügen …" im Kartenfuß neben „Skills hinzufügen" (E61-GUI-Nachtrag, Gegenrichtung
886
+ zur MCP-Ansicht): nur, wo der Harness an diesem Ort MCP kann (mcpKannAn, E16). */
887
+ function mcpHinzufuegenKnopf(ziel) {
888
+ if (!mcpKannAn(ziel)) return null;
889
+ var knopf = e('button', 'knopf leise klein mitsymbol', 'MCP hinzufügen …');
890
+ knopf.type = 'button';
891
+ knopf.insertBefore(harnessZeichen(ziel.harness), knopf.firstChild);
892
+ knopf.setAttribute('aria-label', 'MCP-Server zu ' + zielTitel({ ziel: ziel }) + ' hinzufügen (' + harnessName(ziel.harness) + ')');
893
+ knopf.addEventListener('click', function () { oeffneMcpHinzufuegen(ziel); });
894
+ return knopf;
895
+ }
896
+
897
+ function zielKarte(z) {
898
+ var karte = e('div', 'ziel');
899
+ var kopf = e('div', 'zielkopf');
900
+ kopf.setAttribute('role', 'button');
901
+ kopf.setAttribute('tabindex', '0');
902
+ var schluessel = zielKey(z.ziel);
903
+ var bodyId = 'zielkoerper-' + zielKoerperNr++;
904
+ // Der Filter „Benötigt Aufmerksamkeit" ist eine Absichtserklärung (E45): er zeigt nur betroffene
905
+ // Ziele und klappt sie auf, unabhängig vom gemerkten Zustand, der ihm nicht gehört.
906
+ var offen = zielFilter === 'achtung' || !!ZIEL_OFFEN[schluessel];
907
+ kopf.setAttribute('aria-controls', bodyId);
908
+ kopf.setAttribute('aria-expanded', offen ? 'true' : 'false');
909
+ kopf.appendChild(beschriftet(e('h3', null, zielTitel(z)), z.ziel.harness));
910
+ kopf.appendChild(e('span', 'mono', pfadKurz(z.verzeichnis)));
911
+ if (!harnessAktiv(z.ziel.harness)) kopf.appendChild(e('span', 'ruhend', 'Harness inaktiv'));
912
+ if (zielBraucht(z)) {
913
+ // Zeichen statt Text-Badge: trägt die Farbe selbst (Emoji-Darstellung), aria-label hält den
914
+ // Akkordeon-Knopf-Namen brauchbar (role="img", die Bedeutung steht nicht im Glyph selbst).
915
+ var warnspan = e('span', 'warnung', '⚠️');
916
+ warnspan.setAttribute('role', 'img');
917
+ warnspan.setAttribute('aria-label', 'braucht Aufmerksamkeit');
918
+ kopf.appendChild(warnspan);
919
+ }
920
+ var zahl = z.eintraege.filter(function (ei) { return ei.art !== 'fremd'; }).length;
921
+ kopf.appendChild(e('span', 'zahl', zahl + (zahl === 1 ? ' Link' : ' Links')));
922
+ var pfeil = document.createElementNS('http://www.w3.org/2000/svg', 'svg');
923
+ pfeil.setAttribute('viewBox', '0 0 16 16');
924
+ pfeil.setAttribute('class', 'akkordeonpfeil');
925
+ pfeil.setAttribute('aria-hidden', 'true');
926
+ pfeil.innerHTML = '<path d="M4 6.5 8 10.5 12 6.5"/>';
927
+ kopf.appendChild(pfeil);
928
+ karte.appendChild(kopf);
929
+ var koerper = e('div', 'zielkoerper');
930
+ koerper.id = bodyId;
931
+ koerper.hidden = !offen;
932
+ koerper.appendChild(linkTeil(z));
933
+ karte.appendChild(koerper);
934
+
935
+ function schalte() {
936
+ offen = !offen;
937
+ ZIEL_OFFEN[schluessel] = offen;
938
+ kopf.setAttribute('aria-expanded', offen ? 'true' : 'false');
939
+ koerper.hidden = !offen;
940
+ }
941
+ // Ein Bedienelement in der Kopfzeile (E47 „Skills hinzufügen") schaltet nicht zusätzlich um.
942
+ function ausBedienelement(ev) {
943
+ return ev.target.closest('button, a, input, select, textarea') !== null;
944
+ }
945
+ kopf.addEventListener('click', function (ev) {
946
+ if (!ausBedienelement(ev)) schalte();
947
+ });
948
+ kopf.addEventListener('keydown', function (ev) {
949
+ if (ausBedienelement(ev)) return;
950
+ if (ev.key === 'Enter' || ev.key === ' ' || ev.key === 'Spacebar') {
951
+ ev.preventDefault();
952
+ schalte();
953
+ }
954
+ });
955
+
956
+ return karte;
957
+ }
958
+
959
+ /** Körper des Chips „Alle" einer Verzeichnis-Karte (E53 Pkt. 2, 4, 5): eine gemeinsame Liste
960
+ statt gestapelter Harness-Abschnitte. Eigene Links gebündelt (Schlüssel Name+Quelle), fremde
961
+ Einträge und die Inaktiv-Zeile je Harness mit dessen Zeichen am Fundort. */
962
+ function alleTeil(pfad, teile) {
963
+ var teil = document.createDocumentFragment();
964
+
965
+ teile.forEach(function (z) {
966
+ var eigeneZ = z.eintraege.filter(function (ei) { return ei.art !== 'fremd'; });
967
+ if (!harnessAktiv(z.ziel.harness) && (eigeneZ.length > 0 || z.istRegistriert)) {
968
+ teil.appendChild(ruheZeile(z, eigeneZ.length > 0));
969
+ }
970
+ });
971
+
972
+ var beachteteAlle = [], ignorierteAlle = [];
973
+ teile.forEach(function (z) {
974
+ var ignoriertNamen = z.ignoriert || [];
975
+ z.eintraege.filter(function (ei) { return ei.art === 'fremd'; }).forEach(function (x) {
976
+ var paar = { x: x, z: z };
977
+ if (ignoriertNamen.indexOf(x.name) === -1) beachteteAlle.push(paar);
978
+ else ignorierteAlle.push(paar);
979
+ });
980
+ });
981
+ if (beachteteAlle.length > 0) teil.appendChild(fremdKasten(beachteteAlle, true));
982
+ if (ignorierteAlle.length > 0) teil.appendChild(ignoriertZeile(ignorierteAlle, true));
983
+
984
+ // Eigene Links über alle Harnesses gebündelt: Schlüssel Name+Quelle, hängt ein Name je Harness
985
+ // aus verschiedener Quelle, gibt es je Quelle eine eigene Zeile (E53 Pkt. 2).
986
+ var zeilen = {};
987
+ var reihenfolge = [];
988
+ teile.forEach(function (z) {
989
+ z.eintraege.filter(function (ei) { return ei.art !== 'fremd'; }).forEach(function (ei) {
990
+ var defekt = ei.art === 'link-ohne-quelle';
991
+ var name = defekt ? ei.name : ei.link.name;
992
+ var quelleSchluessel = defekt ? ('defekt\u0000' + ei.checkoutPfad)
993
+ : (quelleKurz(ei.link.checkout.quelle) + '@' + ei.link.checkout.ref);
994
+ // NUL als Trenner, kein Leerzeichen: Skill-Namen dürfen Leerzeichen enthalten, Quellen-Namen
995
+ // seit E59 auch, ein lokaler Pfad ohnehin — „a b“ + „x“ und „a“ + „b x“ ergäben sonst
996
+ // denselben Schlüssel. Immer als Escape geschrieben, nie als rohes Byte im Quelltext.
997
+ var key = name + '\u0000' + quelleSchluessel;
998
+ if (!zeilen[key]) {
999
+ zeilen[key] = {
1000
+ name: name,
1001
+ defekt: defekt,
1002
+ icon: defekt ? { art: 'lokal', pfad: ei.checkoutPfad } : ei.link.checkout.quelle,
1003
+ checkoutPfad: defekt ? ei.checkoutPfad : null,
1004
+ quelle: defekt ? null : ei.link.checkout.quelle,
1005
+ ref: defekt ? null : ei.link.checkout.ref,
1006
+ einschraenkung: defekt ? null : z.einschraenkungen[name],
1007
+ skill: defekt ? null : ei.link.skill,
1008
+ teile: [],
1009
+ };
1010
+ reihenfolge.push(key);
1011
+ }
1012
+ zeilen[key].teile.push(z);
1013
+ });
1014
+ });
1015
+
1016
+ // MCPs hängen am Verzeichnis, nicht am Harness-Chip (E61-GUI-Nachtrag): der Abschnitt unter
1017
+ // „Alle" zeigt jedes Ziel, das an genau diesem Pfad MCP kann — dieselbe Fähigkeitsfrage wie unter
1018
+ // dem Harness-Chip, allein über den Ort aus MCP_PLAETZE gestellt. Kein Harness-Name im Filter
1019
+ // (E16): heute liefert das nur Claude Code, weil Hermes keine Projektebene für MCP kennt; käme
1020
+ // ein Harness mit Projektebene dazu, stünde er hier von selbst und fiele nicht stumm weg.
1021
+ var verzMcpZiele = MCP_PLAETZE.filter(function (p) {
1022
+ return p.ort.art === 'arbeitsverzeichnis' && p.ort.pfad === pfad;
1023
+ });
1024
+
1025
+ if (reihenfolge.length === 0) {
1026
+ var leer = e('div', 'leerzeile');
1027
+ leer.appendChild(e('p', 'ohne', 'Hier liegt noch nichts.'));
1028
+ leer.appendChild(alleSkillsKnopf(pfad, teile));
1029
+ verzMcpZiele.forEach(function (mz) {
1030
+ var mcpKnopfLeer = mcpHinzufuegenKnopf(mz);
1031
+ if (mcpKnopfLeer) leer.appendChild(mcpKnopfLeer);
1032
+ });
1033
+ teil.appendChild(leer);
1034
+ verzMcpZiele.forEach(function (mz) {
1035
+ var mcpAbschnLeer = mcpAbschnitt(mz);
1036
+ if (mcpAbschnLeer) teil.appendChild(mcpAbschnLeer);
1037
+ });
1038
+ return teil;
1039
+ }
1040
+
1041
+ var kopf = e('div', 'linkkopf');
1042
+ kopf.appendChild(e('span'));
1043
+ kopf.appendChild(e('span', 'etikett', 'Skill'));
1044
+ kopf.appendChild(e('span', 'etikett', 'Beschreibung'));
1045
+ kopf.appendChild(e('span', 'etikett', 'Quelle'));
1046
+ kopf.appendChild(e('span'));
1047
+ teil.appendChild(kopf);
1048
+
1049
+ var ul = e('ul', 'linkliste');
1050
+ reihenfolge
1051
+ .slice()
1052
+ .sort(function (ka, kb) {
1053
+ var a = zeilen[ka], b = zeilen[kb];
1054
+ if (a.defekt !== b.defekt) return a.defekt ? -1 : 1;
1055
+ return a.name.localeCompare(b.name, 'de');
1056
+ })
1057
+ .forEach(function (key) {
1058
+ var row = zeilen[key];
1059
+ var harnesse = [];
1060
+ row.teile.forEach(function (z) { if (harnesse.indexOf(z.ziel.harness) === -1) harnesse.push(z.ziel.harness); });
1061
+ harnesse.sort(function (a, b) { return harnessName(a).localeCompare(harnessName(b), 'de'); });
1062
+
1063
+ var li = e('li');
1064
+ li.appendChild(repoIcon(row.icon));
1065
+
1066
+ var nameSpan = e('span', 'name');
1067
+ nameSpan.appendChild(harnesseZeichenSammel(harnesse));
1068
+ nameSpan.appendChild(document.createTextNode(row.name));
1069
+ li.appendChild(nameSpan);
1070
+
1071
+ li.appendChild(wasZelle(row.skill));
1072
+
1073
+ var qz = e('span', 'quelle');
1074
+ if (row.defekt) {
1075
+ qz.appendChild(e('span', 'mono', pfadKurz(row.checkoutPfad)));
1076
+ } else {
1077
+ if (row.einschraenkung) qz.appendChild(document.createTextNode(row.einschraenkung + ' → '));
1078
+ qz.appendChild(quelleLinkElement(row.quelle));
1079
+ var standardRef = STANDARD_REF[quelleKurz(row.quelle)];
1080
+ if (row.ref && standardRef && row.ref !== standardRef) qz.appendChild(e('span', 'ref', '@' + row.ref));
1081
+ // Unterscheiden sich die Gründe je Harness, zählt „über Zeiger", sobald einer davon so gilt.
1082
+ var zeiger = row.teile.some(function (z) { var g = z.gruende[row.name]; return g && g.art === 'zeiger'; });
1083
+ if (zeiger) qz.appendChild(e('span', 'grundmarke', 'über Zeiger'));
1084
+ }
1085
+ li.appendChild(qz);
1086
+
1087
+ var entfernbar = row.teile.filter(function (z) {
1088
+ var g = z.gruende[row.name];
1089
+ return !(g && g.art === 'plugin');
1090
+ });
1091
+ var pluginNamen = [];
1092
+ row.teile.forEach(function (z) {
1093
+ var g = z.gruende[row.name];
1094
+ if (g && g.art === 'plugin' && pluginNamen.indexOf(g.plugin.split('@')[0]) === -1) {
1095
+ pluginNamen.push(g.plugin.split('@')[0]);
1096
+ }
1097
+ });
1098
+ var nichtEntfernbar = row.teile
1099
+ .filter(function (z) { var g = z.gruende[row.name]; return !!(g && g.art === 'plugin'); })
1100
+ .map(function (z) { return z.ziel.harness; });
1101
+
1102
+ if (entfernbar.length > 0) {
1103
+ var weg = e('button', 'knopf leise klein', 'Entfernen');
1104
+ weg.type = 'button';
1105
+ // Die Reichweite gehört in die Beschriftung: unter „Alle" stehen zwei Zeilen gleichen
1106
+ // Namens nebeneinander, sobald ein Skill je Harness aus verschiedener Quelle hängt.
1107
+ weg.setAttribute('aria-label', 'Link auf ' + row.name + ' in '
1108
+ + entfernbar.map(function (z) { return harnessName(z.ziel.harness); }).join(', ') + ' entfernen');
1109
+ weg.addEventListener('click', function () { frageEntfernenAlle(row.name, pfad, entfernbar, nichtEntfernbar); });
1110
+ li.appendChild(weg);
1111
+ } else {
1112
+ li.appendChild(e('span', 'kein-weg', 'nur über Plugin „' + pluginNamen.join('“, „') + '“'));
1113
+ }
1114
+ ul.appendChild(li);
1115
+ });
1116
+ teil.appendChild(ul);
1117
+
1118
+ // MCP-Abschnitt (E61-GUI-Nachtrag): auf Kartenebene unter „Alle", wie unter dem Harness-Chip,
1119
+ // der an diesem Pfad MCP kann.
1120
+ verzMcpZiele.forEach(function (mz) {
1121
+ var mcpAbschnAlle = mcpAbschnitt(mz);
1122
+ if (mcpAbschnAlle) teil.appendChild(mcpAbschnAlle);
1123
+ });
1124
+
1125
+ var fuss = e('div', 'zielfuss');
1126
+ fuss.appendChild(alleSkillsKnopf(pfad, teile));
1127
+ verzMcpZiele.forEach(function (mz) {
1128
+ var mcpKnopfAlle = mcpHinzufuegenKnopf(mz);
1129
+ if (mcpKnopfAlle) fuss.appendChild(mcpKnopfAlle);
1130
+ });
1131
+ teil.appendChild(fuss);
1132
+
1133
+ return teil;
1134
+ }
1135
+
1136
+ /** Chip-Wahl einer Verzeichnis-Karte: merken, Liste neu bauen, Fokus zurück auf den Chip. */
1137
+ function waehleChip(schluessel, wahl) {
1138
+ VERZ_CHIP[schluessel] = wahl;
1139
+ CHIP_FOKUS = { karte: schluessel, wahl: wahl };
1140
+ baueZiele();
1141
+ }
1142
+
1143
+ /** Eine Akkordeon-Karte je Arbeitsverzeichnis (E49), im Körper eine Chip-Leiste „Alle" + je
1144
+ beteiligtem Harness ein Chip (E53): „Alle" zeigt die gebündelte Liste (alleTeil), ein
1145
+ Harness-Chip exakt dessen Einzel-Logik (linkTeil). Der Offen-Zustand läuft über verzKey(pfad). */
1146
+ function verzeichnisKarte(pfad, teile) {
1147
+ var karte = e('div', 'ziel verzeichnis');
1148
+ var kopf = e('div', 'zielkopf');
1149
+ kopf.setAttribute('role', 'button');
1150
+ kopf.setAttribute('tabindex', '0');
1151
+ var schluessel = verzKey(pfad);
1152
+ var bodyId = 'zielkoerper-' + zielKoerperNr++;
1153
+ var offen = zielFilter === 'achtung' || !!ZIEL_OFFEN[schluessel];
1154
+ kopf.setAttribute('aria-controls', bodyId);
1155
+ kopf.setAttribute('aria-expanded', offen ? 'true' : 'false');
1156
+ kopf.appendChild(e('h3', null, pfadKurz(pfad)));
1157
+
1158
+ // Harness-Zeichen aller beteiligten Harnesses statt eines einzelnen (kein Ziel hat hier nur
1159
+ // einen Harness): klein, im Kopf, mit Sammel-aria-label statt einzelner Beschriftung je Icon.
1160
+ var symbole = e('span', 'harnesssymbole');
1161
+ var namen = teile.map(function (z) { return harnessName(z.ziel.harness); }).sort(function (a, b) { return a.localeCompare(b, 'de'); });
1162
+ symbole.setAttribute('role', 'img');
1163
+ symbole.setAttribute('aria-label', namen.join(', '));
1164
+ teile
1165
+ .slice()
1166
+ .sort(function (a, b) { return harnessName(a.ziel.harness).localeCompare(harnessName(b.ziel.harness), 'de'); })
1167
+ .forEach(function (z) { symbole.appendChild(harnessZeichen(z.ziel.harness)); });
1168
+ kopf.appendChild(symbole);
1169
+
1170
+ if (teile.some(zielBraucht)) {
1171
+ var warnspan = e('span', 'warnung', '⚠️');
1172
+ warnspan.setAttribute('role', 'img');
1173
+ warnspan.setAttribute('aria-label', 'braucht Aufmerksamkeit');
1174
+ kopf.appendChild(warnspan);
1175
+ }
1176
+ var gesamtZahl = teile.reduce(function (n, z) {
1177
+ return n + z.eintraege.filter(function (ei) { return ei.art !== 'fremd'; }).length;
1178
+ }, 0);
1179
+ kopf.appendChild(e('span', 'zahl', gesamtZahl + (gesamtZahl === 1 ? ' Link' : ' Links')));
1180
+ var pfeil = document.createElementNS('http://www.w3.org/2000/svg', 'svg');
1181
+ pfeil.setAttribute('viewBox', '0 0 16 16');
1182
+ pfeil.setAttribute('class', 'akkordeonpfeil');
1183
+ pfeil.setAttribute('aria-hidden', 'true');
1184
+ pfeil.innerHTML = '<path d="M4 6.5 8 10.5 12 6.5"/>';
1185
+ kopf.appendChild(pfeil);
1186
+ karte.appendChild(kopf);
1187
+
1188
+ var koerper = e('div', 'zielkoerper');
1189
+ koerper.id = bodyId;
1190
+ koerper.hidden = !offen;
1191
+
1192
+ // Chip-Leiste „Alle" + je beteiligtem Harness einer, Wahl je Karte über VERZ_CHIP (E53 Pkt. 1).
1193
+ var harnesseBeteiligt = [];
1194
+ teile.forEach(function (z) { if (harnesseBeteiligt.indexOf(z.ziel.harness) === -1) harnesseBeteiligt.push(z.ziel.harness); });
1195
+ harnesseBeteiligt.sort(function (a, b) { return harnessName(a).localeCompare(harnessName(b), 'de'); });
1196
+ var aktivChip = VERZ_CHIP[schluessel] || 'alle';
1197
+ if (aktivChip !== 'alle' && harnesseBeteiligt.indexOf(aktivChip) === -1) aktivChip = 'alle';
1198
+
1199
+ var chipleiste = e('div', 'filter kartenchips');
1200
+ chipleiste.setAttribute('role', 'group');
1201
+ chipleiste.setAttribute('aria-label', 'Harness wählen');
1202
+ var chipAlle = e('button', 'chip', 'Alle');
1203
+ chipAlle.type = 'button';
1204
+ chipAlle.dataset.chipkarte = schluessel;
1205
+ chipAlle.dataset.chipwahl = 'alle';
1206
+ chipAlle.setAttribute('aria-pressed', aktivChip === 'alle' ? 'true' : 'false');
1207
+ chipAlle.addEventListener('click', function () { waehleChip(schluessel, 'alle'); });
1208
+ chipleiste.appendChild(chipAlle);
1209
+ harnesseBeteiligt.forEach(function (h) {
1210
+ var chip = e('button', 'chip', harnessName(h));
1211
+ chip.type = 'button';
1212
+ chip.dataset.chipkarte = schluessel;
1213
+ chip.dataset.chipwahl = h;
1214
+ chip.insertBefore(harnessZeichen(h), chip.firstChild);
1215
+ chip.setAttribute('aria-pressed', aktivChip === h ? 'true' : 'false');
1216
+ chip.addEventListener('click', function () { waehleChip(schluessel, h); });
1217
+ chipleiste.appendChild(chip);
1218
+ });
1219
+ if (aktivChip === 'alle') {
1220
+ koerper.appendChild(chipleiste);
1221
+ koerper.appendChild(alleTeil(pfad, teile));
1222
+ } else {
1223
+ var gewaehlt = teile.filter(function (z) { return z.ziel.harness === aktivChip; })[0];
1224
+ // Welcher Ordner im Verzeichnis gelesen wird, stand vorher im Abschnittskopf (25cddf6) und
1225
+ // gehört zum gewählten Harness, nicht zur Karte: er steht rechts in der Chip-Leiste, relativ
1226
+ // zum Verzeichnis, mit der Inaktiv-Marke daneben.
1227
+ if (gewaehlt) {
1228
+ // Nur die Anzeige: liegt der gelesene Ordner unterhalb des Verzeichnisses, steht er
1229
+ // relativ, sonst gekürzt. Beide Trenner, sonst greift das unter Windows nie (E68).
1230
+ var unterhalb = gewaehlt.verzeichnis.indexOf(pfad + '/') === 0
1231
+ || gewaehlt.verzeichnis.indexOf(pfad + '\\') === 0;
1232
+ chipleiste.appendChild(e('span', 'kartenordner mono',
1233
+ unterhalb ? gewaehlt.verzeichnis.slice(pfad.length + 1) : pfadKurz(gewaehlt.verzeichnis)));
1234
+ if (!harnessAktiv(aktivChip)) chipleiste.appendChild(e('span', 'ruhend', 'Harness inaktiv'));
1235
+ }
1236
+ koerper.appendChild(chipleiste);
1237
+ if (gewaehlt) koerper.appendChild(linkTeil(gewaehlt));
1238
+ }
1239
+
1240
+ // Entfernen heißt nur vergessen (E52, E26): der Dialog versichert das, gelöscht wird nichts.
1241
+ var fuss = e('div', 'zielfuss');
1242
+ var weg = e('button', 'knopf leise klein', 'Entfernen …');
1243
+ weg.type = 'button';
1244
+ weg.setAttribute('aria-label', pfadKurz(pfad) + ' aus hmgr entfernen');
1245
+ weg.addEventListener('click', function () { frageVergessen(pfad); });
1246
+ fuss.appendChild(weg);
1247
+ koerper.appendChild(fuss);
1248
+ karte.appendChild(koerper);
1249
+
1250
+ function schalte() {
1251
+ offen = !offen;
1252
+ ZIEL_OFFEN[schluessel] = offen;
1253
+ kopf.setAttribute('aria-expanded', offen ? 'true' : 'false');
1254
+ koerper.hidden = !offen;
1255
+ }
1256
+ function ausBedienelement(ev) {
1257
+ return ev.target.closest('button, a, input, select, textarea') !== null;
1258
+ }
1259
+ kopf.addEventListener('click', function (ev) {
1260
+ if (!ausBedienelement(ev)) schalte();
1261
+ });
1262
+ kopf.addEventListener('keydown', function (ev) {
1263
+ if (ausBedienelement(ev)) return;
1264
+ if (ev.key === 'Enter' || ev.key === ' ' || ev.key === 'Spacebar') {
1265
+ ev.preventDefault();
1266
+ schalte();
1267
+ }
1268
+ });
1269
+
1270
+ return karte;
1271
+ }
1272
+
1273
+ function baueZiele() {
1274
+ var liste = $('zielliste');
1275
+ liste.textContent = '';
1276
+ var karten = zielKarten();
1277
+
1278
+ var agentsSichtbar = karten.agents
1279
+ .filter(function (z) { return zielFilter === 'alle' || zielBraucht(z); })
1280
+ .sort(function (a, b) { return zielTitel(a).localeCompare(zielTitel(b), 'de'); });
1281
+ var verzeichnisseSichtbar = karten.verzeichnisse
1282
+ .filter(function (g) { return zielFilter === 'alle' || g.teile.some(zielBraucht); })
1283
+ .sort(function (a, b) { return pfadKurz(a.pfad).localeCompare(pfadKurz(b.pfad), 'de'); });
1284
+
1285
+ if (agentsSichtbar.length > 0) {
1286
+ liste.appendChild(e('h2', 'gruppenkopf', 'Agents'));
1287
+ agentsSichtbar.forEach(function (z) { liste.appendChild(zielKarte(z)); });
1288
+ }
1289
+ if (verzeichnisseSichtbar.length > 0) {
1290
+ liste.appendChild(e('h2', 'gruppenkopf', 'Arbeitsverzeichnisse'));
1291
+ verzeichnisseSichtbar.forEach(function (g) { liste.appendChild(verzeichnisKarte(g.pfad, g.teile)); });
1292
+ }
1293
+ var gezeigt = agentsSichtbar.length + verzeichnisseSichtbar.length;
1294
+ var gesamt = karten.agents.length + karten.verzeichnisse.length;
1295
+ if (gezeigt === 0)
1296
+ liste.appendChild(e('p', 'leerhinweis', zielFilter === 'achtung'
1297
+ ? 'Kein Ziel braucht gerade Aufmerksamkeit.'
1298
+ : 'Noch keine Ziele. Füge unten ein Arbeitsverzeichnis hinzu oder verlinke einen Skill.'));
1299
+
1300
+ $('zielzahl').textContent = gezeigt + ' von ' + gesamt + ' Karten gezeigt';
1301
+
1302
+ if (CHIP_FOKUS !== null) {
1303
+ var merker = CHIP_FOKUS;
1304
+ CHIP_FOKUS = null;
1305
+ Array.prototype.forEach.call(liste.querySelectorAll('.kartenchips .chip'), function (c) {
1306
+ if (c.dataset.chipkarte === merker.karte && c.dataset.chipwahl === merker.wahl) c.focus();
1307
+ });
1308
+ }
1309
+ }
1310
+
1311
+ document.querySelectorAll('#ansicht-ziele .chip').forEach(function (c) {
1312
+ c.addEventListener('click', function () {
1313
+ zielFilter = c.dataset.zielfilter;
1314
+ // Filterwechsel leert den gemerkten Offen-Zustand (E45): „Alle" beginnt wieder eingeklappt.
1315
+ // Ebenso die Chip-Wahl (E53): sonst zeigte eine Karte unter „Benötigt Aufmerksamkeit" einen
1316
+ // Harness, in dem gar nichts zu sehen ist, während der Grund im Chip daneben steckt.
1317
+ ZIEL_OFFEN = {};
1318
+ VERZ_CHIP = {};
1319
+ document.querySelectorAll('#ansicht-ziele .chip').forEach(function (a) {
1320
+ a.setAttribute('aria-pressed', a === c ? 'true' : 'false');
1321
+ });
1322
+ baueZiele();
1323
+ });
1324
+ });
1325
+
1326
+ // entfernenKontext.eintraege: [{harness, ort}], immer eines bei frageEntfernen (Einzel-Ziel),
1327
+ // mehrere bei frageEntfernenAlle (E53 Pkt. 2, „Alle"-Zeile über mehrere Harnesses).
1328
+ var entfernenKontext = null;
1329
+ function frageEntfernen(name, ziel) {
1330
+ entfernenKontext = { name: name, eintraege: [{ harness: ziel.harness, ort: ziel.ort }] };
1331
+ $('dx-titel').textContent = 'Link auf ' + name + ' entfernen?';
1332
+ $('dx-text').textContent = 'Der Link in ' + zielTitel({ ziel: ziel }) + ' verschwindet. Der Checkout bleibt liegen, bis kein Link mehr darauf zeigt.';
1333
+ zeige($('d-entfernen'));
1334
+ }
1335
+ /** Entfernen einer „Alle"-Zeile (E53 Pkt. 2): entfernbareZiele sind die Ziele (voller z.ziel), in
1336
+ denen der Link weg soll, nichtEntfernbareHarnesse die, in denen er nur über ein Plugin hängt
1337
+ und liegen bleibt. Der Dialog nennt beides. */
1338
+ function frageEntfernenAlle(name, pfad, entfernbareZiele, nichtEntfernbareHarnesse) {
1339
+ entfernenKontext = {
1340
+ name: name,
1341
+ eintraege: entfernbareZiele.map(function (z) { return { harness: z.ziel.harness, ort: z.ziel.ort }; }),
1342
+ };
1343
+ var namen = entfernbareZiele.map(function (z) { return harnessName(z.ziel.harness); });
1344
+ $('dx-titel').textContent = 'Link auf ' + name + ' entfernen?';
1345
+ var text = 'Der Link in ' + namen.join(', ') + ' (' + pfadKurz(pfad) + ') verschwindet.';
1346
+ if (nichtEntfernbareHarnesse.length > 0) {
1347
+ text += ' In ' + nichtEntfernbareHarnesse.map(harnessName).join(', ') + ' bleibt er bestehen (nur über das Plugin).';
1348
+ }
1349
+ text += ' Der Checkout bleibt liegen, bis kein Link mehr darauf zeigt.';
1350
+ $('dx-text').textContent = text;
1351
+ zeige($('d-entfernen'));
1352
+ }
1353
+ nachDialog($('d-entfernen'), async function (wert) {
1354
+ if (wert !== 'ja' || entfernenKontext === null) return;
1355
+ var kontext = entfernenKontext;
1356
+ // Ein Fehlschlag in einem Harness hält die übrigen nicht auf: rufe hat ihn schon gemeldet, die
1357
+ // Schleife läuft weiter, und die Liste wird danach in jedem Fall neu geladen. Sonst stünde nach
1358
+ // einem Fehler die halb erledigte Arbeit unsichtbar in einer veralteten Liste.
1359
+ var gescheitert = [];
1360
+ var hinweiseWeg = [];
1361
+ for (var i = 0; i < kontext.eintraege.length; i++) {
1362
+ var eintrag = kontext.eintraege[i];
1363
+ try {
1364
+ var weg = await rufe(function () {
1365
+ return window.hmgr.linkEntfernen({ skill: kontext.name, harness: eintrag.harness, ort: eintrag.ort });
1366
+ });
1367
+ if (weg && weg.hinweis && hinweiseWeg.indexOf(weg.hinweis) < 0) hinweiseWeg.push(weg.hinweis);
1368
+ } catch (fehler) {
1369
+ gescheitert.push(harnessName(eintrag.harness));
1370
+ }
1371
+ }
1372
+ await ladeAlles();
1373
+ if (gescheitert.length > 1) melde('Der Link auf ' + kontext.name + ' blieb in ' + gescheitert.join(', ') + ' liegen.');
1374
+ // Zeiger-Hinweis (F32) nur, wenn nichts gescheitert ist: sonst stünde er über der Fehlermeldung,
1375
+ // die rufe schon gesetzt hat.
1376
+ else if (gescheitert.length === 0 && hinweiseWeg.length > 0) melde(hinweiseWeg.join('\n'));
1377
+ });
1378
+
1379
+ var loeschenKontext = null;
1380
+ var vergessenPfad = null;
1381
+ function frageVergessen(pfad) {
1382
+ vergessenPfad = pfad;
1383
+ $('dg-text').textContent = pfadKurz(pfad) + ' wird dann nicht mehr von hmgr verwaltet. Das Verzeichnis und alles darin bleiben unverändert erhalten. Zum Wiederaufnehmen fügst du es einfach erneut hinzu.';
1384
+ zeige($('d-vergessen'));
1385
+ }
1386
+ nachDialog($('d-vergessen'), async function (wert) {
1387
+ if (wert !== 'ja' || vergessenPfad === null) return;
1388
+ var pfad = vergessenPfad;
1389
+ vergessenPfad = null;
1390
+ await rufe(function () { return window.hmgr.arbeitsverzeichnisVergessen(pfad); });
1391
+ await ladeAlles();
1392
+ melde(pfadKurz(pfad) + ' aus hmgr entfernt, das Verzeichnis bleibt erhalten.');
1393
+ });
1394
+
1395
+ function frageLoeschen(name, ziel, verzeichnis) {
1396
+ loeschenKontext = { name: name, ziel: ziel };
1397
+ $('dl-titel').textContent = '„' + name + '“ löschen?';
1398
+ $('dl-text').textContent = 'hmgr löscht ' + pfadKurz(verzeichnis) + TRENNER + name
1399
+ + ' unwiderruflich von der Platte, ein Ordner mit allem Inhalt oder ein Link. Das lässt sich nicht rückgängig machen.';
1400
+ zeige($('d-loeschen'));
1401
+ }
1402
+ nachDialog($('d-loeschen'), async function (wert) {
1403
+ if (wert !== 'ja' || loeschenKontext === null) return;
1404
+ var kontext = loeschenKontext;
1405
+ await rufe(function () {
1406
+ return window.hmgr.eintragLoeschen({ harness: kontext.ziel.harness, ort: kontext.ziel.ort, name: kontext.name });
1407
+ });
1408
+ await ladeAlles();
1409
+ });
1410
+
1411
+ var uebernahmeKontext = null;
1412
+ // Lesbare Fassung der Kern-Gründe (UebernahmeGrund), warum ein Eintrag liegen bleibt: eine kurze
1413
+ // Ursache, dazu Handreichungen (E43), die der Nutzer außerhalb von hmgr selbst erledigt.
1414
+ var UEBERNAHME_GRUENDE = {
1415
+ 'kein-link': {
1416
+ ursache: 'Von Hand installiert, hmgr kennt die Herkunft nicht.',
1417
+ tipps: [
1418
+ 'In dein Skills-Repo übernehmen und von dort verlinken',
1419
+ 'Oder als Quelle aufnehmen, falls es ein Git-Repo dazu gibt',
1420
+ ],
1421
+ },
1422
+ 'keine-arbeitskopie': {
1423
+ ursache: 'Der Link führt in keine Git-Arbeitskopie.',
1424
+ tipps: ['Das Linkziel in ein Git-Repo bringen, dann erneut übernehmen'],
1425
+ },
1426
+ 'keine-url': {
1427
+ ursache: 'Die Arbeitskopie hat keine Remote-URL.',
1428
+ tipps: ['Remote hinzufügen und pushen, dann erneut übernehmen'],
1429
+ },
1430
+ 'kein-branch': {
1431
+ ursache: 'Die Arbeitskopie steht auf keinem Branch.',
1432
+ tipps: ['Branch auschecken, dann erneut übernehmen'],
1433
+ },
1434
+ 'skill-unbekannt': {
1435
+ ursache: 'Die Quelle führt unter diesem Pfad keinen Skill.',
1436
+ tipps: ['Prüfen, ob der Skill im Repo verschoben oder entfernt wurde'],
1437
+ },
1438
+ 'lokale-aenderungen': {
1439
+ ursache: 'Der Ordner weicht von der Quelle ab.',
1440
+ tipps: ['Änderungen committen und pushen, dann erneut übernehmen'],
1441
+ },
1442
+ 'nicht-pruefbar': {
1443
+ ursache: 'hmgr kann nicht prüfen, ob der Ordner der Quelle entspricht.',
1444
+ tipps: ['Skill über die skills-CLI neu installieren, dann erneut prüfen'],
1445
+ },
1446
+ };
1447
+ async function oeffneUebernahme(ziel, name) {
1448
+ uebernahmeKontext = { ziel: ziel, name: name };
1449
+ $('du-etikett').textContent = 'Probelauf, nichts wird verändert';
1450
+ $('du-titel').textContent = '„' + name + '“ in ' + zielTitel({ ziel: ziel }) + ' übernehmen?';
1451
+ var ergebnis = await rufe(function () {
1452
+ return window.hmgr.uebernehmen({ harness: ziel.harness, ort: ziel.ort, name: name, probelauf: true });
1453
+ });
1454
+ var ul = $('du-liste');
1455
+ ul.textContent = '';
1456
+ ergebnis.eintraege.forEach(function (ei) {
1457
+ var li = e('li');
1458
+ if (ei.ergebnis === 'uebernommen') {
1459
+ li.appendChild(e('span', 'ok', 'wird ersetzt'));
1460
+ var s = e('span');
1461
+ s.appendChild(e('b', null, ei.name));
1462
+ s.appendChild(quelleSpan('mono', ei.quelle, ' @ ' + ei.ref + ' (' + ei.skill.pfad + ')'));
1463
+ if (ei.ordnerErsetzt) s.appendChild(e('p', 'fremderklaerung', 'Der Ordner wird gelöscht und durch den Link ersetzt.'));
1464
+ if (ei.gitignoriert && ei.gitignoriert.length > 0) {
1465
+ var maxAnzeige = 5;
1466
+ var angezeigt = ei.gitignoriert.slice(0, maxAnzeige).join(', ');
1467
+ var weitere = ei.gitignoriert.length > maxAnzeige ? ', …' : '';
1468
+ s.appendChild(e('p', 'fremderklaerung', zahlwort(ei.gitignoriert.length, 'gitignorierte Datei geht', 'gitignorierte Dateien gehen')
1469
+ + ' beim Ersetzen verloren: ' + angezeigt + weitere));
1470
+ }
1471
+ li.appendChild(s);
1472
+ } else {
1473
+ li.appendChild(e('span', 'aus', 'bleibt liegen'));
1474
+ var s2 = e('span');
1475
+ s2.appendChild(e('b', null, ei.name));
1476
+ var grundInfo = UEBERNAHME_GRUENDE[ei.grund];
1477
+ s2.appendChild(e('span', 'mono', grundInfo ? grundInfo.ursache : ei.grund));
1478
+ if (grundInfo && grundInfo.tipps && grundInfo.tipps.length > 0) {
1479
+ var tippbox = e('div', 'tipps');
1480
+ tippbox.appendChild(e('p', 'tippkopf', 'Außerhalb von hmgr:'));
1481
+ var tul = e('ul', 'tippliste');
1482
+ grundInfo.tipps.forEach(function (t) { tul.appendChild(e('li', null, '→ ' + t)); });
1483
+ tippbox.appendChild(tul);
1484
+ s2.appendChild(tippbox);
1485
+ }
1486
+ li.appendChild(s2);
1487
+ }
1488
+ ul.appendChild(li);
1489
+ });
1490
+ if (ergebnis.eintraege.length === 0) ul.appendChild(e('li', null, 'Nichts mehr zu übernehmen.'));
1491
+ var ersetzt = ergebnis.eintraege.filter(function (ei) { return ei.ergebnis === 'uebernommen'; }).length;
1492
+ var liegen = ergebnis.eintraege.length - ersetzt;
1493
+ $('du-bilanz').textContent = ersetzt === 0
1494
+ ? 'Nichts wird ersetzt.'
1495
+ : (ersetzt === 1 ? '1 wird ersetzt' : ersetzt + ' werden ersetzt') + ', '
1496
+ + (liegen === 0 ? 'nichts bleibt liegen' : liegen === 1 ? '1 bleibt liegen' : liegen + ' bleiben liegen');
1497
+ // Ohne Übernehmbares gibt es nichts auszuführen: Knopf weg statt stumm deaktiviert.
1498
+ $('du-los').hidden = ersetzt === 0;
1499
+ zeige($('d-uebernahme'));
1500
+ }
1501
+ nachDialog($('d-uebernahme'), async function (wert) {
1502
+ if (wert !== 'los' || uebernahmeKontext === null) return;
1503
+ var ziel = uebernahmeKontext.ziel;
1504
+ var name = uebernahmeKontext.name;
1505
+ var ergebnis = await rufe(function () {
1506
+ return window.hmgr.uebernehmen({ harness: ziel.harness, ort: ziel.ort, name: name, probelauf: false });
1507
+ });
1508
+ var uebernommen = ergebnis.eintraege.filter(function (ei) { return ei.ergebnis === 'uebernommen'; }).length;
1509
+ var uebersprungen = ergebnis.eintraege.length - uebernommen;
1510
+ $('de-titel').textContent = 'Übernommen';
1511
+ $('de-text').textContent = uebernommen + ' übernommen, ' + uebersprungen + ' übersprungen.';
1512
+ setzeErfolgHinweise([ergebnis.hinweis]);
1513
+ var ul = $('de-liste');
1514
+ ul.textContent = '';
1515
+ ergebnis.eintraege.forEach(function (ei) {
1516
+ var li = e('li');
1517
+ li.appendChild(e('span', ei.ergebnis === 'uebernommen' ? 'ok' : 'aus', ei.ergebnis === 'uebernommen' ? 'Link' : 'Liegen gelassen'));
1518
+ var s = e('span');
1519
+ s.appendChild(e('b', null, ei.name));
1520
+ li.appendChild(s);
1521
+ ul.appendChild(li);
1522
+ });
1523
+ await ladeAlles();
1524
+ setTimeout(function () { zeige($('d-erfolg')); }, 90);
1525
+ });
1526
+
1527
+ $('verzneu').addEventListener('click', function () {
1528
+ $('dv-pfad').value = '';
1529
+ zeige($('d-verzeichnis'));
1530
+ });
1531
+ enterBestaetigt($('dv-pfad'), $('dv-los'));
1532
+ $('dv-auswaehlen').addEventListener('click', async function () {
1533
+ var pfad = await rufe(function () { return window.hmgr.verzeichnisWaehlen(); });
1534
+ if (pfad) $('dv-pfad').value = pfad;
1535
+ });
1536
+ nachDialog($('d-verzeichnis'), async function (wert) {
1537
+ if (wert !== 'los') return;
1538
+ var pfad = $('dv-pfad').value.trim();
1539
+ if (!pfad) return;
1540
+ var ergebnis = await rufe(function () { return window.hmgr.arbeitsverzeichnisHinzufuegen(pfad); });
1541
+ await ladeAlles();
1542
+ $('de-titel').textContent = 'Arbeitsverzeichnis hinzugefügt';
1543
+ $('de-text').textContent = ergebnis.hinweis || ('Zeiger: ' + (ergebnis.zeigerPfad || 'keiner'));
1544
+ setzeErfolgHinweise([]);
1545
+ $('de-liste').textContent = '';
1546
+ setTimeout(function () { zeige($('d-erfolg')); }, 90);
1547
+ });
1548
+
1549
+ /* ================= SKILLS ================= */
1550
+
1551
+ var QUELLFARBEN = ['', 'q2', 'q3'];
1552
+ function quellenIndex(quelleText) {
1553
+ var i = QUELLEN.findIndex(function (q) { return q.text === quelleText; });
1554
+ return i < 0 ? 0 : i % 3;
1555
+ }
1556
+
1557
+ function baueQuellenfilter() {
1558
+ var box = $('quellenfilter');
1559
+ box.textContent = '';
1560
+ var alle = e('button', 'chip', 'Alle');
1561
+ alle.type = 'button';
1562
+ alle.dataset.filter = 'alle';
1563
+ alle.setAttribute('aria-pressed', quellenFilter === 'alle' ? 'true' : 'false');
1564
+ alle.addEventListener('click', function () { setzeQuellenfilter('alle'); });
1565
+ box.appendChild(alle);
1566
+
1567
+ var gesehen = {};
1568
+ SKILLS.forEach(function (s) {
1569
+ if (gesehen[s.quelleSchluessel]) return;
1570
+ gesehen[s.quelleSchluessel] = true;
1571
+ var idx = quellenIndex(s.quelleText);
1572
+ var chip = e('button', 'chip', s.quelleName);
1573
+ chip.title = s.quelleText;
1574
+ chip.type = 'button';
1575
+ chip.dataset.filter = s.quelleSchluessel;
1576
+ chip.setAttribute('aria-pressed', quellenFilter === s.quelleSchluessel ? 'true' : 'false');
1577
+ var punkt = e('i');
1578
+ punkt.style.background = 'var(--q' + (idx + 1) + ')';
1579
+ chip.insertBefore(punkt, chip.firstChild);
1580
+ chip.addEventListener('click', function () { setzeQuellenfilter(s.quelleSchluessel); });
1581
+
1582
+ // Quellen-Chip-Paar (E60): links filtert der Name, rechts öffnet das Pfeilsymbol die Quelle
1583
+ // (Repo-Seite im Browser, lokale Arbeitskopie im Dateimanager). Kein <a> im <button>, deshalb
1584
+ // stehen sie nebeneinander in einer gemeinsamen Hülle.
1585
+ var quelle = QUELLEN.filter(function (q) { return q.text === s.quelleText; })[0];
1586
+ if (quelle) {
1587
+ var gruppe = e('span', 'chipgruppe');
1588
+ gruppe.appendChild(chip);
1589
+ var lokal = quelle.art === 'lokal';
1590
+ var oeffnen = document.createElement(lokal ? 'button' : 'a');
1591
+ if (lokal) oeffnen.type = 'button'; else oeffnen.href = '#';
1592
+ oeffnen.className = 'chipoeffnen';
1593
+ oeffnen.title = quelle.text;
1594
+ oeffnen.setAttribute('aria-label', s.quelleName + ' ' + (lokal ? 'im Dateimanager öffnen' : 'im Browser öffnen'));
1595
+ oeffnen.appendChild(externSvg());
1596
+ oeffnen.addEventListener('click', function (ev) {
1597
+ ev.preventDefault();
1598
+ ev.stopPropagation();
1599
+ window.hmgr.quelleOeffnen(quelle.text).catch(function () {});
1600
+ });
1601
+ gruppe.appendChild(oeffnen);
1602
+ box.appendChild(gruppe);
1603
+ } else {
1604
+ box.appendChild(chip);
1605
+ }
1606
+ });
1607
+ }
1608
+
1609
+ function setzeQuellenfilter(wert) {
1610
+ quellenFilter = wert;
1611
+ document.querySelectorAll('#quellenfilter .chip').forEach(function (c) {
1612
+ c.setAttribute('aria-pressed', c.dataset.filter === wert ? 'true' : 'false');
1613
+ });
1614
+ filtereSkills();
1615
+ }
1616
+
1617
+ /** Immer sichtbare Marken einer Skill-Zeile aus dem Frontmatter (E55), nur wenn zutreffend.
1618
+ Hooks und vorab erlaubte Werkzeuge sind sicherheitsrelevant und deshalb warnend gefärbt (F11). */
1619
+ function skillMarken(skill) {
1620
+ var marken = [];
1621
+ if (skill.nurPerBefehl) marken.push({ text: 'nur per /' + skill.name });
1622
+ if (skill.nichtPerBefehl) marken.push({ text: 'nicht per /' + skill.name });
1623
+ if (skill.registriertHooks) marken.push({ text: 'registriert Hooks', warnend: true });
1624
+ if (skill.werkzeugeErlaubt) marken.push({ text: 'Werkzeuge vorab erlaubt', warnend: true });
1625
+ return marken;
1626
+ }
1627
+
1628
+ /** Die Marken eines Skills als fertige Box, für Katalog und Skills-hinzufügen-Dialog (F13).
1629
+ Ohne Besonderheiten null, dann bleibt die Zeile unverändert. */
1630
+ function skillMarkenBox(skill) {
1631
+ var marken = skillMarken(skill);
1632
+ if (marken.length === 0) return null;
1633
+ var box = e('span', 'marken');
1634
+ marken.forEach(function (m) { box.appendChild(e('span', 'merker leise' + (m.warnend ? ' warnend' : ''), m.text)); });
1635
+ return box;
1636
+ }
1637
+
1638
+ /** Beschreibungs-Zelle der Ziel-Tabellen: die Beschreibung (E39) und darunter die immer
1639
+ sichtbaren Frontmatter-Marken (E55), derselbe Helfer wie im Katalog. Ohne Skill (Link ohne
1640
+ Quelle) bleibt nur die Fehlt-Zeile. */
1641
+ function wasZelle(skill) {
1642
+ var feld = e('span', 'wasfeld');
1643
+ var beschreibung = skill ? skill.beschreibung : undefined;
1644
+ if (beschreibung) {
1645
+ var wasEl = e('span', 'was', beschreibung);
1646
+ if (beschreibung.length > 88) wasEl.title = beschreibung;
1647
+ feld.appendChild(wasEl);
1648
+ } else {
1649
+ feld.appendChild(e('span', 'was fehlt', 'keine Beschreibung in der SKILL.md'));
1650
+ }
1651
+ var marken = skill ? skillMarkenBox(skill) : null;
1652
+ if (marken) feld.appendChild(marken);
1653
+ return feld;
1654
+ }
1655
+
1656
+ /** Zeilen des Aufklappbereichs einer Skill-Zeile aus dem Frontmatter (E55), nur wenn vorhanden. */
1657
+ function skillDetails(skill) {
1658
+ var zeilen = [];
1659
+ if (skill.kompatibilitaet) zeilen.push({ label: 'Kompatibilität', text: skill.kompatibilitaet });
1660
+ if (skill.modell) zeilen.push({ label: 'Modell', text: skill.modell });
1661
+ if (skill.eigenerKontext) zeilen.push({ label: 'Kontext', text: 'läuft in eigenem Kontext' });
1662
+ if (skill.argumentHinweis) zeilen.push({ label: 'Argumente', text: skill.argumentHinweis });
1663
+ return zeilen;
1664
+ }
1665
+
1666
+ function baueSkills() {
1667
+ var liste = $('katalog');
1668
+ liste.textContent = '';
1669
+ SKILLS.forEach(function (skill, i) {
1670
+ var li = e('li', 'zeile');
1671
+ li.dataset.index = i;
1672
+ var idx = quellenIndex(skill.quelleText);
1673
+ li.style.borderLeftColor = 'var(--q' + (idx + 1) + ')';
1674
+
1675
+ var links = e('div');
1676
+ var titel = e('h3');
1677
+ var herkunftQuelle = QUELLEN.filter(function (q) { return q.text === skill.quelleText; })[0];
1678
+ // Quellen-Icon vor dem Namen, wie in den Ziel-Tabellen (E60). Ohne bekannte Quelle generisch.
1679
+ titel.appendChild(repoIcon(herkunftQuelle || { art: 'lokal', pfad: skill.quelleText }));
1680
+ titel.appendChild(document.createTextNode(skill.name));
1681
+ var markenBox = skillMarkenBox(skill);
1682
+ if (markenBox) titel.appendChild(markenBox);
1683
+ links.appendChild(titel);
1684
+ links.appendChild(e('p', 'beschreibung', skill.beschreibung || ''));
1685
+ // Herkunft: der Name der Quelle, anklickbar wie überall sonst (E60), die Adresse steht im
1686
+ // title. Fehlt die Quelle in der Liste (Katalog und Quellen kämen auseinander), bleibt es
1687
+ // beim reinen Text — angeschrieben ist die Zeile so oder so.
1688
+ var herkunft = e('p', 'herkunft mono');
1689
+ if (herkunftQuelle) {
1690
+ herkunft.appendChild(quelleLinkElement(herkunftQuelle));
1691
+ herkunft.appendChild(e('b', null, ' · Ref ' + skill.ref));
1692
+ } else {
1693
+ herkunft.title = skill.quelleText;
1694
+ herkunft.appendChild(e('b', null, skill.quelleName + ' · Ref ' + skill.ref));
1695
+ }
1696
+ links.appendChild(herkunft);
1697
+
1698
+ var rechts = e('div', 'rechts');
1699
+ var details = skillDetails(skill);
1700
+ var umschalter = null;
1701
+ var koerperId = 'skillkoerper-' + i;
1702
+ // Der Aufklappzustand überlebt den Neuaufbau durch ladeAlles (F10), wie ZIEL_OFFEN.
1703
+ var offen = !!SKILL_OFFEN[skillKey(skill)];
1704
+ if (details.length > 0) {
1705
+ umschalter = e('button', 'aufklappknopf');
1706
+ umschalter.type = 'button';
1707
+ umschalter.setAttribute('aria-expanded', offen ? 'true' : 'false');
1708
+ umschalter.setAttribute('aria-controls', koerperId);
1709
+ umschalter.setAttribute('aria-label', 'Details zu ' + skill.name + ' ein- oder ausklappen');
1710
+ var pfeil = document.createElementNS('http://www.w3.org/2000/svg', 'svg');
1711
+ pfeil.setAttribute('viewBox', '0 0 16 16');
1712
+ pfeil.setAttribute('class', 'akkordeonpfeil dunkel');
1713
+ pfeil.setAttribute('aria-hidden', 'true');
1714
+ pfeil.innerHTML = '<path d="M4 6.5 8 10.5 12 6.5"/>';
1715
+ umschalter.appendChild(pfeil);
1716
+ rechts.appendChild(umschalter);
1717
+ }
1718
+ var knopf = e('button', 'knopf leise', 'Verlinken');
1719
+ knopf.type = 'button';
1720
+ knopf.addEventListener('click', function () { starteVerlinken(i); });
1721
+ rechts.appendChild(knopf);
1722
+
1723
+ li.appendChild(links);
1724
+ li.appendChild(rechts);
1725
+
1726
+ if (details.length > 0 && umschalter) {
1727
+ var koerper = e('div', 'skillkoerper');
1728
+ koerper.id = koerperId;
1729
+ koerper.hidden = !offen;
1730
+ details.forEach(function (d) {
1731
+ var zeile = e('p', 'skilldetail');
1732
+ zeile.appendChild(e('b', null, d.label + ': '));
1733
+ zeile.appendChild(document.createTextNode(d.text));
1734
+ koerper.appendChild(zeile);
1735
+ });
1736
+ li.appendChild(koerper);
1737
+ umschalter.addEventListener('click', function () {
1738
+ var war = umschalter.getAttribute('aria-expanded') === 'true';
1739
+ umschalter.setAttribute('aria-expanded', war ? 'false' : 'true');
1740
+ koerper.hidden = war;
1741
+ SKILL_OFFEN[skillKey(skill)] = !war;
1742
+ });
1743
+ }
1744
+
1745
+ liste.appendChild(li);
1746
+ });
1747
+ filtereSkills();
1748
+ }
1749
+
1750
+ function filtereSkills() {
1751
+ var suche = $('suchfeld').value.trim().toLowerCase();
1752
+ var sichtbar = 0;
1753
+ document.querySelectorAll('#katalog .zeile').forEach(function (li) {
1754
+ var skill = SKILLS[+li.dataset.index];
1755
+ var passtSuche = !suche || skill.name.toLowerCase().indexOf(suche) > -1 || skill.pfad.toLowerCase().indexOf(suche) > -1;
1756
+ var passtFilter = quellenFilter === 'alle' || skill.quelleSchluessel === quellenFilter;
1757
+ var an = passtSuche && passtFilter;
1758
+ li.hidden = !an;
1759
+ if (an) sichtbar++;
1760
+ });
1761
+ $('katalogzahl').textContent = sichtbar + ' von ' + SKILLS.length + ' Skills gezeigt';
1762
+ }
1763
+ $('suchfeld').addEventListener('input', filtereSkills);
1764
+
1765
+ var laufendeSkill = null;
1766
+ function starteVerlinken(index) {
1767
+ laufendeSkill = index;
1768
+ var skill = SKILLS[index];
1769
+ $('dz-skill').textContent = skill.name;
1770
+ var box = $('dz-ziele');
1771
+ box.textContent = '';
1772
+ ZIELE.forEach(function (z) {
1773
+ var schon = z.eintraege.some(function (ei) {
1774
+ var n = ei.art === 'link' ? ei.link.name : ei.name;
1775
+ return n === skill.name;
1776
+ });
1777
+ var label = e('label', 'wahlzeile');
1778
+ var input = document.createElement('input');
1779
+ input.type = 'checkbox';
1780
+ input.value = String(ZIELE.indexOf(z));
1781
+ input.disabled = schon;
1782
+ label.appendChild(input);
1783
+ var txt = e('span');
1784
+ txt.appendChild(beschriftet(e('b', null, zielTitel(z)), z.ziel.harness));
1785
+ txt.appendChild(e('span', 'mono', pfadKurz(z.verzeichnis)));
1786
+ if (schon) txt.appendChild(e('span', 'notiz', 'Hängt hier schon.'));
1787
+ label.appendChild(txt);
1788
+ box.appendChild(label);
1789
+ });
1790
+ zeige($('d-ziel'));
1791
+ }
1792
+
1793
+ $('d-ziel').addEventListener('submit', function (ev) {
1794
+ if (ev.submitter && ev.submitter.value === 'los') {
1795
+ var gewaehlt = Array.from($('dz-ziele').querySelectorAll('input:checked'));
1796
+ if (gewaehlt.length === 0) ev.preventDefault();
1797
+ }
1798
+ });
1799
+
1800
+ nachDialog($('d-ziel'), async function (wert) {
1801
+ if (wert !== 'los' || laufendeSkill === null) return;
1802
+ var skill = SKILLS[laufendeSkill];
1803
+ var indizes = Array.from($('dz-ziele').querySelectorAll('input:checked')).map(function (i) { return +i.value; });
1804
+ var ergebnisZeilen = [];
1805
+ var hinweise = [];
1806
+ for (var n = 0; n < indizes.length; n++) {
1807
+ var z = ZIELE[indizes[n]];
1808
+ try {
1809
+ var gelegt = await rufe(function () {
1810
+ return window.hmgr.linkLegen({ skill: skill.name, harness: z.ziel.harness, ort: z.ziel.ort, quelle: skill.quelleText });
1811
+ });
1812
+ hinweise.push(gelegt.hinweis);
1813
+ ergebnisZeilen.push({ ok: true, ziel: z });
1814
+ } catch (fehler) {
1815
+ ergebnisZeilen.push({ ok: false, ziel: z, fehler: fehler.message });
1816
+ }
1817
+ }
1818
+ await ladeAlles();
1819
+ $('de-titel').textContent = 'Verlinkt';
1820
+ var erfolge = ergebnisZeilen.filter(function (r) { return r.ok; }).length;
1821
+ $('de-text').textContent = skill.name + ' hängt jetzt in ' + erfolge + (erfolge === 1 ? ' Ziel.' : ' Zielen.');
1822
+ setzeErfolgHinweise(hinweise);
1823
+ var ul = $('de-liste');
1824
+ ul.textContent = '';
1825
+ ergebnisZeilen.forEach(function (r) {
1826
+ var li = e('li');
1827
+ li.appendChild(e('span', r.ok ? 'ok' : 'aus', r.ok ? 'Link' : 'Fehler'));
1828
+ var s = e('span');
1829
+ s.appendChild(beschriftet(e('b', null, zielTitel(r.ziel)), r.ziel.ziel.harness));
1830
+ s.appendChild(e('span', 'mono', r.ok ? pfadKurz(r.ziel.verzeichnis) + TRENNER + skill.name : r.fehler));
1831
+ li.appendChild(s);
1832
+ ul.appendChild(li);
1833
+ });
1834
+ setTimeout(function () { zeige($('d-erfolg')); }, 90);
1835
+ });
1836
+
1837
+ /* E47: Gegenrichtung zu starteVerlinken/d-ziel. Mehrfachauswahl über den ganzen Katalog, schon
1838
+ hier hängende Skills (gleicher Name, egal welche Art) sind ausgegraut. E53 erweitert das um
1839
+ einen zweiten Modus für den Chip „Alle" einer Verzeichnis-Karte: modus „alle" grauet einen
1840
+ Skill nur aus, wenn er in ALLEN beteiligten Harnesses hängt, und verlinkt je Harness, wo er fehlt. */
1841
+ var skillsKontext = null;
1842
+ function dskBilanzAktualisieren() {
1843
+ var n = $('dsk-liste').querySelectorAll('input:checked').length;
1844
+ $('dsk-bilanz').textContent = zahlwort(n, 'ausgewählt', 'ausgewählt');
1845
+ }
1846
+ function filtereDskListe() {
1847
+ var suche = $('dsk-suche').value.trim().toLowerCase();
1848
+ document.querySelectorAll('#dsk-liste .wahlzeile').forEach(function (label) {
1849
+ var skill = SKILLS[+label.dataset.index];
1850
+ label.hidden = !!suche && skill.name.toLowerCase().indexOf(suche) === -1;
1851
+ });
1852
+ }
1853
+ $('dsk-suche').addEventListener('input', filtereDskListe);
1854
+
1855
+ function oeffneSkillsHinzufuegen(z) {
1856
+ skillsKontext = { modus: 'einzeln', z: z };
1857
+ $('dsk-ziel').textContent = zielTitel(z);
1858
+ $('dsk-suche').value = '';
1859
+ var box = $('dsk-liste');
1860
+ box.textContent = '';
1861
+ var vorhandeneNamen = z.eintraege.map(function (ei) { return ei.art === 'link' ? ei.link.name : ei.name; });
1862
+ SKILLS.forEach(function (skill, i) {
1863
+ var schon = vorhandeneNamen.indexOf(skill.name) > -1;
1864
+ var label = e('label', 'wahlzeile quellfarbe');
1865
+ label.dataset.index = i;
1866
+ var idx = quellenIndex(skill.quelleText);
1867
+ label.style.borderLeftColor = 'var(--q' + (idx + 1) + ')';
1868
+ var input = document.createElement('input');
1869
+ input.type = 'checkbox';
1870
+ input.value = String(i);
1871
+ input.disabled = schon;
1872
+ input.addEventListener('change', dskBilanzAktualisieren);
1873
+ label.appendChild(input);
1874
+ var txt = e('span');
1875
+ txt.appendChild(e('b', null, skill.name));
1876
+ // Dieselben Marken wie im Katalog (F13): hier fällt die Entscheidung, was installiert wird.
1877
+ var marken = skillMarkenBox(skill);
1878
+ if (marken) txt.appendChild(marken);
1879
+ var quelleSpanEl = e('span', 'mono', skill.quelleName + ' · Ref ' + skill.ref);
1880
+ quelleSpanEl.title = skill.quelleText;
1881
+ txt.appendChild(quelleSpanEl);
1882
+ if (schon) txt.appendChild(e('span', 'notiz', 'Hängt hier schon.'));
1883
+ label.appendChild(txt);
1884
+ box.appendChild(label);
1885
+ });
1886
+ dskBilanzAktualisieren();
1887
+ zeige($('d-skills'));
1888
+ $('dsk-suche').focus();
1889
+ }
1890
+
1891
+ /** Skills-hinzufügen-Dialog für den Chip „Alle" (E53 Pkt. 4): ausgegraut nur, wenn ein Skill in
1892
+ allen beteiligten Harnesses schon hängt. */
1893
+ function oeffneSkillsHinzufuegenAlle(pfad, teile) {
1894
+ skillsKontext = { modus: 'alle', pfad: pfad, teile: teile };
1895
+ $('dsk-ziel').textContent = pfadKurz(pfad) + ' (alle Harnesses)';
1896
+ $('dsk-suche').value = '';
1897
+ var box = $('dsk-liste');
1898
+ box.textContent = '';
1899
+ var vorhandeneJeHarness = teile.map(function (z) {
1900
+ return z.eintraege.map(function (ei) { return ei.art === 'link' ? ei.link.name : ei.name; });
1901
+ });
1902
+ SKILLS.forEach(function (skill, i) {
1903
+ var fehltIn = teile.filter(function (z, n) { return vorhandeneJeHarness[n].indexOf(skill.name) === -1; });
1904
+ var ueberall = fehltIn.length === 0;
1905
+ var label = e('label', 'wahlzeile quellfarbe');
1906
+ label.dataset.index = i;
1907
+ var idx = quellenIndex(skill.quelleText);
1908
+ label.style.borderLeftColor = 'var(--q' + (idx + 1) + ')';
1909
+ var input = document.createElement('input');
1910
+ input.type = 'checkbox';
1911
+ input.value = String(i);
1912
+ input.disabled = ueberall;
1913
+ input.addEventListener('change', dskBilanzAktualisieren);
1914
+ label.appendChild(input);
1915
+ var txt = e('span');
1916
+ txt.appendChild(e('b', null, skill.name));
1917
+ // Dieselben Marken wie im Katalog (F13): hier fällt die Entscheidung, was installiert wird.
1918
+ var marken = skillMarkenBox(skill);
1919
+ if (marken) txt.appendChild(marken);
1920
+ var quelleSpanEl = e('span', 'mono', skill.quelleName + ' · Ref ' + skill.ref);
1921
+ quelleSpanEl.title = skill.quelleText;
1922
+ txt.appendChild(quelleSpanEl);
1923
+ if (ueberall) txt.appendChild(e('span', 'notiz', 'Hängt schon überall.'));
1924
+ else if (fehltIn.length < teile.length) {
1925
+ txt.appendChild(e('span', 'notiz', 'Hängt schon in ' + (teile.length - fehltIn.length) + ' von ' + teile.length + ' Harnesses.'));
1926
+ }
1927
+ label.appendChild(txt);
1928
+ box.appendChild(label);
1929
+ });
1930
+ dskBilanzAktualisieren();
1931
+ zeige($('d-skills'));
1932
+ $('dsk-suche').focus();
1933
+ }
1934
+
1935
+ $('d-skills').addEventListener('submit', function (ev) {
1936
+ if (ev.submitter && ev.submitter.value === 'los') {
1937
+ var gewaehlt = Array.from($('dsk-liste').querySelectorAll('input:checked'));
1938
+ if (gewaehlt.length === 0) ev.preventDefault();
1939
+ }
1940
+ });
1941
+
1942
+ nachDialog($('d-skills'), async function (wert) {
1943
+ if (wert !== 'los' || skillsKontext === null) return;
1944
+ var kontext = skillsKontext;
1945
+ var indizes = Array.from($('dsk-liste').querySelectorAll('input:checked')).map(function (i) { return +i.value; });
1946
+
1947
+ if (kontext.modus === 'einzeln') {
1948
+ var z = kontext.z;
1949
+ var ergebnisZeilen = [];
1950
+ // Zwei Skills gleichen Namens aus verschiedenen Quellen stehen beide im Katalog (E32), am Ziel
1951
+ // gilt der Name aber nur einmal (E12), und der Kern ersetzte den eben gelegten Link stumm.
1952
+ // Der zweite gehört daher als Fehler in die Bilanz, sonst meldet der Dialog Links, die es nicht gibt.
1953
+ var vergeben = [];
1954
+ var hinweiseZiel = [];
1955
+ for (var n = 0; n < indizes.length; n++) {
1956
+ var skill = SKILLS[indizes[n]];
1957
+ if (vergeben.indexOf(skill.name) > -1) {
1958
+ ergebnisZeilen.push({
1959
+ ok: false,
1960
+ skill: skill,
1961
+ fehler: 'Ein Skill dieses Namens ist gerade verlinkt worden, hier gibt es den Namen nur einmal.',
1962
+ });
1963
+ continue;
1964
+ }
1965
+ try {
1966
+ var gelegtZiel = await rufe(function () {
1967
+ return window.hmgr.linkLegen({ skill: skill.name, harness: z.ziel.harness, ort: z.ziel.ort, quelle: skill.quelleText });
1968
+ });
1969
+ hinweiseZiel.push(gelegtZiel.hinweis);
1970
+ vergeben.push(skill.name);
1971
+ ergebnisZeilen.push({ ok: true, skill: skill });
1972
+ } catch (fehler) {
1973
+ ergebnisZeilen.push({ ok: false, skill: skill, fehler: fehler.message });
1974
+ }
1975
+ }
1976
+ // Nach dem Verlinken bleibt die Karte offen, in der das Ziel steckt: bei einem
1977
+ // Arbeitsverzeichnis ist das die gebündelte Verzeichnis-Karte, nicht das Ziel allein (E49).
1978
+ ZIEL_OFFEN[kartenKey(z.ziel)] = true;
1979
+ await ladeAlles();
1980
+ var erfolge = ergebnisZeilen.filter(function (r) { return r.ok; }).length;
1981
+ $('de-titel').textContent = 'Verlinkt';
1982
+ $('de-text').textContent = zahlwort(erfolge, 'Skill hängt', 'Skills hängen') + ' jetzt in ' + zielTitel(z) + '.';
1983
+ setzeErfolgHinweise(hinweiseZiel);
1984
+ var ul = $('de-liste');
1985
+ ul.textContent = '';
1986
+ ergebnisZeilen.forEach(function (r) {
1987
+ var li = e('li');
1988
+ li.appendChild(e('span', r.ok ? 'ok' : 'aus', r.ok ? 'Link' : 'Fehler'));
1989
+ var s = e('span');
1990
+ s.appendChild(e('b', null, r.skill.name));
1991
+ if (!r.ok) s.appendChild(e('span', 'mono', r.fehler));
1992
+ li.appendChild(s);
1993
+ ul.appendChild(li);
1994
+ });
1995
+ setTimeout(function () { zeige($('d-erfolg')); }, 90);
1996
+ return;
1997
+ }
1998
+
1999
+ // modus „alle" (E53 Pkt. 4): je gewähltem Skill in jedem Harness verlinken, wo er noch fehlt.
2000
+ // Erfolgsliste je Harness eine Zeile, schon vorhandene Harnesses bleiben ohne Zeile.
2001
+ var pfad = kontext.pfad;
2002
+ var teile = kontext.teile;
2003
+ var zeilenErgebnis = [];
2004
+ // Der Duplikat-Schutz aus dem Einzel-Modus gilt hier je Harness, nicht über die Auswahl hinweg:
2005
+ // derselbe Skill gehört absichtlich in mehrere Harnesses, zwei gleichnamige Skills aus
2006
+ // verschiedenen Quellen (E32) gehören aber in keinem Harness zweimal (E12), der Kern ersetzte
2007
+ // den eben gelegten Link sonst stumm.
2008
+ var vergebenJeHarness = {};
2009
+ var hinweiseAlle = [];
2010
+ for (var m = 0; m < indizes.length; m++) {
2011
+ var sk = SKILLS[indizes[m]];
2012
+ for (var t = 0; t < teile.length; t++) {
2013
+ var zt = teile[t];
2014
+ var vorhandene = zt.eintraege.map(function (ei) { return ei.art === 'link' ? ei.link.name : ei.name; });
2015
+ if (vorhandene.indexOf(sk.name) > -1) continue;
2016
+ var vergebenKey = zt.ziel.harness + '\u0000' + sk.name;
2017
+ if (vergebenJeHarness[vergebenKey]) {
2018
+ zeilenErgebnis.push({
2019
+ ok: false,
2020
+ skill: sk,
2021
+ harness: zt.ziel.harness,
2022
+ fehler: 'Ein Skill dieses Namens ist gerade verlinkt worden, hier gibt es den Namen nur einmal.',
2023
+ });
2024
+ continue;
2025
+ }
2026
+ try {
2027
+ var gelegtAlle = await rufe(function () {
2028
+ return window.hmgr.linkLegen({ skill: sk.name, harness: zt.ziel.harness, ort: zt.ziel.ort, quelle: sk.quelleText });
2029
+ });
2030
+ hinweiseAlle.push(gelegtAlle.hinweis);
2031
+ vergebenJeHarness[vergebenKey] = true;
2032
+ zeilenErgebnis.push({ ok: true, skill: sk, harness: zt.ziel.harness });
2033
+ } catch (fehler) {
2034
+ zeilenErgebnis.push({ ok: false, skill: sk, harness: zt.ziel.harness, fehler: fehler.message });
2035
+ }
2036
+ }
2037
+ }
2038
+ VERZ_CHIP[verzKey(pfad)] = 'alle';
2039
+ ZIEL_OFFEN[verzKey(pfad)] = true;
2040
+ await ladeAlles();
2041
+ var erfolgeAlle = zeilenErgebnis.filter(function (r) { return r.ok; }).length;
2042
+ $('de-titel').textContent = 'Verlinkt';
2043
+ $('de-text').textContent = zahlwort(erfolgeAlle, 'Link angelegt', 'Links angelegt') + ' in ' + pfadKurz(pfad) + '.';
2044
+ setzeErfolgHinweise(hinweiseAlle);
2045
+ var ul2 = $('de-liste');
2046
+ ul2.textContent = '';
2047
+ zeilenErgebnis.forEach(function (r) {
2048
+ var li = e('li');
2049
+ li.appendChild(e('span', r.ok ? 'ok' : 'aus', r.ok ? 'Link' : 'Fehler'));
2050
+ var s = e('span');
2051
+ // Derselbe Skill steht hier je Harness einmal. Der Harness gehört als Text dazu, das Zeichen
2052
+ // allein trägt keine Beschriftung und die Zeilen wären sonst nicht zu unterscheiden.
2053
+ s.appendChild(beschriftet(e('b', null, r.skill.name), r.harness));
2054
+ s.appendChild(e('span', 'mono', harnessName(r.harness) + (r.ok ? '' : ' · ' + r.fehler)));
2055
+ li.appendChild(s);
2056
+ ul2.appendChild(li);
2057
+ });
2058
+ setTimeout(function () { zeige($('d-erfolg')); }, 90);
2059
+ });
2060
+
2061
+ /* ================= AKTUALISIEREN ================= */
2062
+
2063
+ /**
2064
+ * Ein Aktualisieren-Lauf, von Hand über den Knopf oder selbsttätig angestoßen (E44). Eine Sperre
2065
+ * über den disabled-Zustand des Knopfs verhindert, dass ein Anstoß während eines laufenden Laufs
2066
+ * etwas tut. Automatische Läufe rufen den Kern ohne Rückfrage, sie räumen also nichts weg.
2067
+ */
2068
+ /* Stand-Zeile der Seitenleiste (Entwurf): hervorgehoben „N Quellen mit Neuem" nach einem
2069
+ automatischen Lauf mit Änderungen, bis die Update-Ansicht den Bericht zeigt; sonst ruhig
2070
+ „Alles aktuell, HH:MM geprüft", oder bei Fehlschlägen (E51) „Prüfung fehlgeschlagen, HH:MM"
2071
+ (alle Quellen-Abrufe gescheitert) bzw. „Geprüft mit Fehlern, HH:MM" (Mischung). Das „mit
2072
+ Neuem"-Signal hat Vorrang: es kündigt sich sofort nach einem automatischen Lauf an, die
2073
+ Fehlerkennzeichnung gilt erst wieder im ruhigen Zustand danach. */
2074
+ var standZeit = null;
2075
+ var standFehlerZustand = 'keine';
2076
+ function standRuhig() {
2077
+ var text = 'Alles aktuell, ' + standZeit + ' geprüft';
2078
+ if (standZeit === null) text = 'Noch nicht aktualisiert';
2079
+ else if (standFehlerZustand === 'alle') text = 'Prüfung fehlgeschlagen, ' + standZeit;
2080
+ else if (standFehlerZustand === 'gemischt') text = 'Geprüft mit Fehlern, ' + standZeit;
2081
+ $('standtext').textContent = text;
2082
+ $('standknopf').classList.remove('neu');
2083
+ $('standknopf').textContent = 'Prüfen';
2084
+ }
2085
+ function standNachLauf(ergebnis, automatisch, uhrzeit) {
2086
+ standZeit = uhrzeit;
2087
+ var quellen = {};
2088
+ ergebnis.checkouts.forEach(function (c) {
2089
+ if (c.aktualisierung.ergebnis === 'aktualisiert') quellen[quelleKurz(c.checkout.quelle)] = true;
2090
+ });
2091
+ var neue = Object.keys(quellen).length;
2092
+ var fehlgeschlagen = ergebnis.checkouts.filter(function (c) { return c.aktualisierung.ergebnis === 'fehlgeschlagen'; }).length;
2093
+ var gesamt = ergebnis.checkouts.length;
2094
+ standFehlerZustand = fehlgeschlagen === 0 ? 'keine' : fehlgeschlagen === gesamt ? 'alle' : 'gemischt';
2095
+ // Von Hand läuft es in der Update-Ansicht, der Bericht ist schon sichtbar: nichts anzukündigen.
2096
+ if (neue === 0 || !automatisch || !$('ansicht-update').hidden) return standRuhig();
2097
+ var p = $('standtext');
2098
+ p.textContent = '';
2099
+ p.appendChild(e('b', null, zahlwort(neue, 'Quelle', 'Quellen')));
2100
+ p.appendChild(document.createTextNode(' mit Neuem'));
2101
+ $('standknopf').classList.add('neu');
2102
+ $('standknopf').textContent = 'Ansehen';
2103
+ }
2104
+
2105
+ async function aktualisierenLauf(automatisch) {
2106
+ var knopf = $('updatestart');
2107
+ if (knopf.disabled) return;
2108
+ knopf.disabled = true;
2109
+ knopf.textContent = 'Läuft …';
2110
+ $('standtext').textContent = 'Prüft …';
2111
+ try {
2112
+ var ergebnis = await rufe(function () {
2113
+ return automatisch ? window.hmgr.aktualisieren({ rueckfragen: false }) : window.hmgr.aktualisieren();
2114
+ });
2115
+ baueBericht(ergebnis);
2116
+ var uhrzeit = new Date().toLocaleTimeString('de-DE', { hour: '2-digit', minute: '2-digit' });
2117
+ var ausloeser = automatisch ? 'automatisch' : 'von Hand';
2118
+ $('updatestand').textContent = 'Zuletzt ' + uhrzeit + ', ' + ausloeser + ', ' + ergebnis.checkouts.length + ' Checkouts.';
2119
+ standNachLauf(ergebnis, automatisch, uhrzeit);
2120
+ await ladeAlles();
2121
+ } finally {
2122
+ knopf.disabled = false;
2123
+ knopf.textContent = 'Jetzt aktualisieren';
2124
+ // Scheiterte der Lauf, bliebe sonst „Prüft …" stehen.
2125
+ if ($('standtext').textContent === 'Prüft …') standRuhig();
2126
+ }
2127
+ }
2128
+
2129
+ $('updatestart').addEventListener('click', function () { aktualisierenLauf(false); });
2130
+ // Der Standknopf sagt, was ein Klick tut: ruhig „Prüfen" (Ansicht öffnen und Lauf starten),
2131
+ // mit Neuem „Ansehen" (nur den Bericht zeigen, der Lauf ist schon gelaufen). Die Klasse muss
2132
+ // vor zeigeAnsicht gelesen werden, denn die räumt den neu-Zustand ab.
2133
+ $('standknopf').addEventListener('click', function () {
2134
+ var neu = this.classList.contains('neu');
2135
+ zeigeAnsicht('update');
2136
+ if (!neu) aktualisierenLauf(false);
2137
+ });
2138
+ window.hmgr.onAktualisierenAnstossen(function () { aktualisierenLauf(true); });
2139
+
2140
+ // Stilles Neuladen bei Änderungen außerhalb der GUI (E62, Fokus-Wechsel und Datei-Watcher): nur
2141
+ // ladeAlles, keine Meldung, kein Spinner-Zwang. Nachrutsch-sicher: höchstens ein Durchlauf läuft,
2142
+ // höchstens ein weiterer ist vorgemerkt, ein laufender Durchlauf wird nie von einem zweiten
2143
+ // überholt. Fehler landen wie sonst über rufe() in der Meldungsleiste, kein stiller Absturz.
2144
+ var stillerReloadLaeuft = false;
2145
+ var stillerReloadVorgemerkt = false;
2146
+ async function stillerReload() {
2147
+ // Läuft gerade ein Aktualisieren-Lauf, wird nicht parallel geladen: er endet ohnehin mit
2148
+ // ladeAlles und nimmt die Änderung mit. Sonst bauten zwei Läufe gleichzeitig die Ansicht auf.
2149
+ if ($('updatestart').disabled) return;
2150
+ if (stillerReloadLaeuft) {
2151
+ stillerReloadVorgemerkt = true;
2152
+ return;
2153
+ }
2154
+ stillerReloadLaeuft = true;
2155
+ try {
2156
+ await ladeAlles();
2157
+ } catch (fehler) {
2158
+ // rufe() hat den Fehler schon gemeldet, hier bleibt nur: den Durchlauf nicht wiederholen.
2159
+ } finally {
2160
+ stillerReloadLaeuft = false;
2161
+ if (stillerReloadVorgemerkt) {
2162
+ stillerReloadVorgemerkt = false;
2163
+ void stillerReload();
2164
+ }
2165
+ }
2166
+ }
2167
+ window.hmgr.onNeuLadenAnstossen(function () { void stillerReload(); });
2168
+
2169
+ function baueBericht(ergebnis) {
2170
+ var box = $('bericht');
2171
+ box.textContent = '';
2172
+ box.hidden = false;
2173
+
2174
+ var checkoutBlock = e('div', 'block');
2175
+ checkoutBlock.appendChild(e('span', 'etikett', 'Checkouts'));
2176
+ var cul = e('ul');
2177
+ if (ergebnis.checkouts.length === 0) cul.appendChild(e('li', null, 'Keine Checkouts.'));
2178
+ ergebnis.checkouts.forEach(function (c) {
2179
+ var li = e('li');
2180
+ var a = c.aktualisierung;
2181
+ var art = a.ergebnis === 'aktualisiert' || a.ergebnis === 'unveraendert' ? 'ok' : 'aus';
2182
+ var text = a.ergebnis === 'aktualisiert' ? 'aktualisiert'
2183
+ : a.ergebnis === 'unveraendert' ? 'unverändert'
2184
+ : a.ergebnis === 'fehlgeschlagen' ? 'fehlgeschlagen (' + a.fehler + ')'
2185
+ : 'übersprungen (lokale Änderung)';
2186
+ li.appendChild(e('span', 'marke-' + art, text));
2187
+ li.appendChild(quelleSpan('pfad', c.checkout.quelle, ' @ ' + c.checkout.ref));
2188
+ cul.appendChild(li);
2189
+ });
2190
+ checkoutBlock.appendChild(cul);
2191
+ box.appendChild(checkoutBlock);
2192
+
2193
+ if (ergebnis.pluginHarnesses.length > 0) {
2194
+ var pluginBlock = e('div', 'block');
2195
+ pluginBlock.appendChild(e('span', 'etikett', 'Plugins'));
2196
+ var pul = e('ul');
2197
+ ergebnis.pluginHarnesses.forEach(function (name) {
2198
+ var li = e('li');
2199
+ li.appendChild(e('span', 'marke-ok', 'aktualisiert'));
2200
+ li.appendChild(e('span', 'pfad', name));
2201
+ pul.appendChild(li);
2202
+ });
2203
+ pluginBlock.appendChild(pul);
2204
+ box.appendChild(pluginBlock);
2205
+ }
2206
+
2207
+ if (ergebnis.aufraeumen.verwaiste.length > 0) {
2208
+ var raeumBlock = e('div', 'block entfernt');
2209
+ raeumBlock.appendChild(e('span', 'etikett', 'Verwaiste Checkouts'));
2210
+ var rul = e('ul');
2211
+ ergebnis.aufraeumen.verwaiste.forEach(function (v) {
2212
+ var li = e('li');
2213
+ var text = v.geloescht ? 'gelöscht' : v.lokaleAenderung ? 'liegen gelassen (lokale Änderung)' : 'liegen gelassen';
2214
+ li.appendChild(e('span', v.geloescht ? 'marke-aus' : 'marke-aus', text));
2215
+ li.appendChild(quelleSpan('pfad', v.checkout.quelle, ' @ ' + v.checkout.ref));
2216
+ rul.appendChild(li);
2217
+ });
2218
+ raeumBlock.appendChild(rul);
2219
+ box.appendChild(raeumBlock);
2220
+ }
2221
+ }
2222
+
2223
+ /* ================= PLUGINS ================= */
2224
+
2225
+ function baueMarketplaces() {
2226
+ var box = $('pluginliste');
2227
+ box.textContent = '';
2228
+ if (MARKETPLACES.length === 0) {
2229
+ box.appendChild(e('p', 'leerhinweis', 'Keine Quelle mit marketplace.json bekannt.'));
2230
+ return;
2231
+ }
2232
+ MARKETPLACES.forEach(function (m) {
2233
+ var markt = e('div', 'markt');
2234
+ var kopf = e('div', 'marktkopf');
2235
+ kopf.appendChild(e('h3', null, m.name));
2236
+ kopf.appendChild(e('span', 'mono', m.quelleText));
2237
+ markt.appendChild(kopf);
2238
+ m.plugins.forEach(function (plugin) {
2239
+ var zeile = e('div', 'pluginzeile');
2240
+ var links = e('div');
2241
+ links.appendChild(e('h4', null, plugin.name));
2242
+ links.appendChild(e('p', null, plugin.skills.length + (plugin.skills.length === 1 ? ' Skill' : ' Skills') +
2243
+ ': ' + plugin.skills.map(function (s) { return s.name; }).join(', ')));
2244
+ if (plugin.dependencies.length > 0) {
2245
+ var kette = e('div', 'kette');
2246
+ kette.appendChild(e('span', 'glied', plugin.name));
2247
+ plugin.dependencies.forEach(function (dep) {
2248
+ kette.appendChild(e('span', 'pfeil', '→'));
2249
+ kette.appendChild(e('span', 'glied', dep));
2250
+ });
2251
+ links.appendChild(kette);
2252
+ }
2253
+ zeile.appendChild(links);
2254
+ markt.appendChild(zeile);
2255
+ });
2256
+ box.appendChild(markt);
2257
+ });
2258
+ }
2259
+
2260
+ /* ================= MCP (E61-Folgerunde, F46: reiner Katalog) ================= */
2261
+ /* Der Katalog aus ~/.hmgr/mcp.yaml wird hier nicht bearbeitet, hmgr liest ihn nur: kein Anlegen,
2262
+ kein Ändern, nur Einhängen (entwurf/gui.html). Abweichung vom Entwurf (F43): kein Erfolgsdialog
2263
+ und kein Wirkungs-Vermerk an der Orts-Zeile, Erfolg läuft über die bestehende Meldungsleiste
2264
+ (melde()), bei Claude Code mit dem Hinweis des Kerns zur nächsten Sitzung angehängt, bei Hermes
2265
+ ohne (der Kern liefert den Hinweis nur für Claude Code).
2266
+ F46 („die Ziele gehören nicht in die MCP, sondern die MCP in die Ziele"): wo ein Server hängt und
2267
+ das Aushängen zeigen seither nur noch die Ziel-Karten (mcpAbschnitt weiter unten), diese Ansicht
2268
+ bleibt reiner Katalog — Name, Beschreibung, Homepage, Konfigblock, F39-Warnzeile, Notizen,
2269
+ Einhängen. mcpOrtDatei/-Scope/-Spur bleiben hier stehen, sie tragen weiterhin den Einhängen-
2270
+ Dialog (baueMcpWahl) und den Aushängen-Text (frageMcpAus), beide von den Ziel-Karten geteilt. */
2271
+
2272
+ /* Wohin der Eintrag geschrieben wird, rein zur Anzeige: Claude Code führt Projekt- wie globale
2273
+ Server in ~/.claude.json (die geteilte .mcp.json der Projektwurzel bleibt unangetastet, E57),
2274
+ Hermes je Ort in seiner config.yaml. */
2275
+ function mcpOrtDatei(ziel) {
2276
+ if (ziel.harness === 'hermes')
2277
+ return ziel.ort.art === 'profil'
2278
+ ? pfadSchreibweise('~/.hermes/profiles/' + ziel.ort.name + '/config.yaml')
2279
+ : pfadSchreibweise('~/.hermes/config.yaml');
2280
+ return pfadSchreibweise('~/.claude.json');
2281
+ }
2282
+ function mcpOrtScope(ziel) {
2283
+ if (ziel.harness !== 'claude-code') return null;
2284
+ return ziel.ort.art === 'arbeitsverzeichnis' ? 'Scope local' : 'Scope user';
2285
+ }
2286
+ /** Was in der Ort-Zeile hinter dem Namen steht: die Datei mit dem Scope. Anders als im Entwurf
2287
+ auch beim Arbeitsverzeichnis, denn der Ortsname der App trägt den Pfad schon (`ortName`); im
2288
+ Entwurf hieß er nur „Arbeitsverzeichnis", weshalb der Pfad dort hier stand. Jede Angabe
2289
+ einmal: Pfad im Namen, Datei und Scope daneben. */
2290
+ function mcpOrtSpur(ziel) {
2291
+ var scope = mcpOrtScope(ziel);
2292
+ return mcpOrtDatei(ziel) + (scope ? ' · ' + scope : '');
2293
+ }
2294
+
2295
+ function mcpKonfigArt(konfig) { return 'command' in konfig ? 'stdio' : 'HTTP'; }
2296
+ /** Der Konfigblock im Wortlaut, so wie er in der mcp.yaml steht: die ${VAR}-Verweise darin bleiben
2297
+ sichtbar, nur sie sind der Grund für die Warnzeile darunter. Nie ein Klartext-Secret (F41). */
2298
+ function mcpKonfigWortlaut(konfig) {
2299
+ if ('command' in konfig) {
2300
+ var text = [konfig.command].concat(konfig.args || []).join(' ');
2301
+ Object.keys(konfig.env || {}).forEach(function (k) { text += ' · ' + k + '=' + konfig.env[k]; });
2302
+ return text;
2303
+ }
2304
+ var url = konfig.url;
2305
+ Object.keys(konfig.headers || {}).forEach(function (k) { url += ' · ' + k + ': ' + konfig.headers[k]; });
2306
+ return url;
2307
+ }
2308
+
2309
+ /** Die Homepage eines Servers, dasselbe Muster wie eine Quelle (E60): der Name bleibt ruhig, das
2310
+ Signalorange trägt nur das Pfeilsymbol. Öffnet über denselben Kanal wie eine Quelle (E60): eine
2311
+ http(s)-Adresse geht in den Browser. */
2312
+ function mcpHeimLink(url) {
2313
+ var a = document.createElement('a');
2314
+ a.className = 'qlink';
2315
+ a.href = '#';
2316
+ a.title = url;
2317
+ a.setAttribute('aria-label', url + ' im Browser öffnen');
2318
+ a.appendChild(document.createTextNode(url));
2319
+ a.appendChild(externSvg());
2320
+ a.addEventListener('click', function (ev) {
2321
+ ev.preventDefault();
2322
+ window.hmgr.quelleOeffnen(url).catch(function () {});
2323
+ });
2324
+ return a;
2325
+ }
2326
+
2327
+ /** F39: die Prüf-Funktion ist dieselbe wie beim tatsächlichen Einhängen, hier aber nur gegen die
2328
+ Prozess-Umgebung von hmgr (der Main-Prozess liefert `fehlendeVariablen` schon fertig mit der
2329
+ Übersicht, ohne den ortsbezogenen .env-Abgleich, der bleibt dem Einhängen selbst vorbehalten
2330
+ und kommt dann als `warnung` zurück). Wiederverwendet die bestehende .ruhezeile-Optik. */
2331
+ function mcpWarnZeile(fehlend) {
2332
+ var mehrzahl = fehlend.length > 1;
2333
+ var namen = fehlend.map(function (v) { return '${' + v + '}'; }).join(', ');
2334
+ var rz = e('div', 'ruhezeile');
2335
+ var svg = document.createElementNS('http://www.w3.org/2000/svg', 'svg');
2336
+ svg.setAttribute('viewBox', '0 0 16 16');
2337
+ svg.setAttribute('stroke-width', '1.6');
2338
+ svg.setAttribute('aria-hidden', 'true');
2339
+ svg.innerHTML = '<circle cx="8" cy="8" r="6.5"/><path d="M8 4.5v4M8 10.5v.5"/>';
2340
+ rz.appendChild(svg);
2341
+ rz.appendChild(e('p', null, namen + (mehrzahl ? ' sind' : ' ist') + ' in der Umgebung von hmgr nicht gesetzt. Ob '
2342
+ + (mehrzahl ? 'die Variablen' : 'die Variable') + ' beim Start des Harness gesetzt ' + (mehrzahl ? 'sind' : 'ist')
2343
+ + ', kann hmgr nicht wissen.'));
2344
+ return rz;
2345
+ }
2346
+
2347
+ /** Notizen sind das Langsame am Eintrag (entwurf/gui.html): zugeklappt, öffnen sich nur, wenn
2348
+ jemand sie sucht. Der Wortlaut bleibt, wie er in der mcp.yaml steht, Zeilenumbrüche
2349
+ eingeschlossen (weiss-raum: pre-line in styles.css). */
2350
+ function mcpNotizenBlock(text) {
2351
+ var d = document.createElement('details');
2352
+ d.className = 'notizen';
2353
+ var s = document.createElement('summary');
2354
+ var pfeil = document.createElementNS('http://www.w3.org/2000/svg', 'svg');
2355
+ pfeil.setAttribute('viewBox', '0 0 16 16');
2356
+ pfeil.setAttribute('class', 'keil');
2357
+ pfeil.setAttribute('fill', 'none');
2358
+ pfeil.setAttribute('stroke', 'currentColor');
2359
+ pfeil.setAttribute('stroke-width', '1.7');
2360
+ pfeil.setAttribute('stroke-linejoin', 'round');
2361
+ pfeil.setAttribute('aria-hidden', 'true');
2362
+ pfeil.innerHTML = '<path d="M4.3 2.5 7.8 6 4.3 9.5"/>';
2363
+ s.appendChild(pfeil);
2364
+ s.appendChild(document.createTextNode('Notizen'));
2365
+ d.appendChild(s);
2366
+ d.appendChild(e('p', null, text));
2367
+ return d;
2368
+ }
2369
+
2370
+ function mcpKarte(k) {
2371
+ var eintrag = k.eintrag;
2372
+ var kEl = e('div', 'mcp');
2373
+
2374
+ var kopf = e('div', 'mcpkopf');
2375
+ kopf.appendChild(e('b', null, eintrag.name));
2376
+ if (eintrag.homepage) kopf.appendChild(mcpHeimLink(eintrag.homepage));
2377
+ var ein = e('button', 'knopf leise klein', 'Einhängen …');
2378
+ ein.type = 'button';
2379
+ ein.setAttribute('aria-label', eintrag.name + ' einhängen');
2380
+ ein.addEventListener('click', function () { oeffneMcpEinhaengen(k); });
2381
+ kopf.appendChild(ein);
2382
+ kEl.appendChild(kopf);
2383
+
2384
+ kEl.appendChild(e('p', 'was', eintrag.beschreibung));
2385
+
2386
+ var konf = e('div', 'kette mcpkonfig');
2387
+ konf.appendChild(e('span', 'glied', mcpKonfigArt(eintrag.konfig)));
2388
+ konf.appendChild(e('span', 'mono', mcpKonfigWortlaut(eintrag.konfig)));
2389
+ kEl.appendChild(konf);
2390
+
2391
+ if (k.fehlendeVariablen.length > 0) kEl.appendChild(mcpWarnZeile(k.fehlendeVariablen));
2392
+ if (eintrag.notizen) kEl.appendChild(mcpNotizenBlock(eintrag.notizen));
2393
+
2394
+ return kEl;
2395
+ }
2396
+
2397
+ /** Ohne Katalog steht hier kein leerer Kasten, sondern der Weg zur Datei: anlegen lässt sich ein
2398
+ Eintrag nur dort, hmgr schreibt sie nie (E61). */
2399
+ function mcpEinladung() {
2400
+ var kasten = e('div', 'startfeld mcpstart');
2401
+ var p1 = e('p');
2402
+ p1.appendChild(document.createTextNode('Noch kein Server im Katalog. hmgr liest ihn aus '));
2403
+ p1.appendChild(e('span', 'mono', pfadSchreibweise('~/.hmgr/mcp.yaml')));
2404
+ p1.appendChild(document.createTextNode(' und schreibt die Datei nie.'));
2405
+ kasten.appendChild(p1);
2406
+ var p2 = e('p');
2407
+ p2.appendChild(document.createTextNode('Trag einen Server dort von Hand ein, oder lass ihn von dem LLM eintragen, das ihn ohnehin gerade einrichtet. Das Format steht in '));
2408
+ p2.appendChild(e('span', 'mono', 'hmgr help mcp'));
2409
+ p2.appendChild(document.createTextNode('.'));
2410
+ kasten.appendChild(p2);
2411
+ return kasten;
2412
+ }
2413
+
2414
+ function baueMcp() {
2415
+ var w = $('mcpliste');
2416
+ w.textContent = '';
2417
+ if (MCP_KATALOG.length === 0) w.appendChild(mcpEinladung());
2418
+ else MCP_KATALOG.forEach(function (k) { w.appendChild(mcpKarte(k)); });
2419
+ $('navzahlmcp').textContent = MCP_KATALOG.length;
2420
+ }
2421
+
2422
+ /* ---------------- Einhängen (F42) ---------------- */
2423
+
2424
+ var mcpLaufend = null;
2425
+ var mcpPlaetzeAuswahl = [];
2426
+ // Sperre gegen Doppel-Anstoß (wie beim Hinzufügen einer Quelle, E54): zwischen Klick und Dialog
2427
+ // liegt ein IPC-Aufruf. Ein zweiter Klick in dieser Lücke riefe showModal() an einem schon
2428
+ // offenen Dialog auf, und das ist ein Fehler, kein zweiter Dialog.
2429
+ var mcpWahlOeffnet = false;
2430
+
2431
+ async function oeffneMcpEinhaengen(k) {
2432
+ if (mcpWahlOeffnet || $('d-mcp').open) return;
2433
+ mcpWahlOeffnet = true;
2434
+ try {
2435
+ await baueMcpWahl(k);
2436
+ } finally {
2437
+ mcpWahlOeffnet = false;
2438
+ }
2439
+ }
2440
+
2441
+ async function baueMcpWahl(k) {
2442
+ mcpLaufend = k;
2443
+ $('dm-name').textContent = k.eintrag.name;
2444
+ $('dm-text').textContent = 'Ein Ort je Einhängen. agents kennt keine MCP-Server und steht deshalb nicht zur Wahl.';
2445
+ var box = $('dm-orte');
2446
+ box.textContent = '';
2447
+ // Die Plätze kommen aus dem Kern (F42, E16): die Fähigkeit hängt am Adapter, nicht am
2448
+ // Harness-Namen, hier wird nicht selbst entschieden, wer welche Ebene kennt.
2449
+ var plaetze = await rufe(function () { return window.hmgr.mcpPlaetze(); });
2450
+ mcpPlaetzeAuswahl = plaetze;
2451
+ plaetze.forEach(function (ziel, i) {
2452
+ var schon = k.eingehaengt.some(function (z) { return zielKey(z) === zielKey(ziel); });
2453
+ var aktiv = harnessAktiv(ziel.harness);
2454
+ var label = e('label', 'wahlzeile');
2455
+ var input = document.createElement('input');
2456
+ input.type = 'radio';
2457
+ input.name = 'mcport';
2458
+ input.value = String(i);
2459
+ input.disabled = schon || !aktiv;
2460
+ label.appendChild(input);
2461
+
2462
+ var txt = e('span');
2463
+ txt.appendChild(beschriftet(e('b', null, zielTitel({ ziel: ziel })), ziel.harness));
2464
+ txt.appendChild(e('span', 'mono', mcpOrtSpur(ziel)));
2465
+
2466
+ if (schon) {
2467
+ // F46: die Orte-Liste am Katalog-Eintrag gibt es nicht mehr, der Weg zum Aushängen führt
2468
+ // über den Entfernen-Knopf im MCP-Abschnitt der Ziel-Karte.
2469
+ txt.appendChild(e('span', 'notiz', 'Hängt hier schon. Zum Aushängen den Entfernen-Knopf am MCP-Abschnitt der Ziel-Karte benutzen.'));
2470
+ } else if (!aktiv) {
2471
+ txt.appendChild(e('span', 'notiz', harnessName(ziel.harness) + ' ist inaktiv. Schalt den Harness in den Einstellungen wieder ein, um hier einzuhängen.'));
2472
+ } else if (ziel.ort.art === 'arbeitsverzeichnis') {
2473
+ // E64: die Datei bleibt beim Einhängen unangetastet, aber nicht mehr grundsätzlich — der
2474
+ // Übernehmen-Knopf an der Ziel-Karte fasst sie auf ausdrücklichen Auftrag an.
2475
+ txt.appendChild(e('span', 'notiz', 'Gilt nur für dich. Eingehängt wird neben der geteilten .mcp.json des Projekts, die bleibt dabei unverändert.'));
2476
+ } else if (ziel.harness === 'hermes' && ziel.ort.art === 'home') {
2477
+ txt.appendChild(e('span', 'notiz', 'Hermes kennt keine Projektebene für MCP-Server, hier geht nur global.'));
2478
+ }
2479
+ label.appendChild(txt);
2480
+ box.appendChild(label);
2481
+ });
2482
+ zeige($('d-mcp'));
2483
+ }
2484
+
2485
+ $('d-mcp').addEventListener('submit', function (ev) {
2486
+ if (ev.submitter && ev.submitter.value === 'los') {
2487
+ var gewaehlt = $('dm-orte').querySelector('input:checked');
2488
+ if (!gewaehlt) ev.preventDefault();
2489
+ }
2490
+ });
2491
+
2492
+ nachDialog($('d-mcp'), async function (wert) {
2493
+ var k = mcpLaufend;
2494
+ mcpLaufend = null;
2495
+ if (wert !== 'los' || !k) return;
2496
+ var gewaehlt = $('dm-orte').querySelector('input:checked');
2497
+ if (!gewaehlt) return;
2498
+ var ziel = mcpPlaetzeAuswahl[+gewaehlt.value];
2499
+ var ergebnis = await rufe(function () {
2500
+ return window.hmgr.mcpEinhaengen({ name: k.eintrag.name, harness: ziel.harness, ort: ziel.ort });
2501
+ });
2502
+ await ladeAlles();
2503
+ // Erfolg über die Meldungsleiste (F43): kein Erfolgsdialog, kein Vermerk an der Orts-Zeile. Der
2504
+ // Kern liefert den Hinweis zur Wirkung nur bei Claude Code, Hermes lädt selbst nach.
2505
+ var teile = [k.eintrag.name + ' hängt jetzt in ' + zielTitel({ ziel: ziel }) + '.'];
2506
+ if (ergebnis.hinweis) teile.push(ergebnis.hinweis);
2507
+ if (ergebnis.warnung) teile.push(ergebnis.warnung);
2508
+ melde(teile.join('\n'));
2509
+ });
2510
+
2511
+ /* ---------------- Aushängen ---------------- */
2512
+
2513
+ var mcpAusKontext = null;
2514
+ function frageMcpAus(eintrag, ziel) {
2515
+ mcpAusKontext = { eintrag: eintrag, ziel: ziel };
2516
+ $('dma-titel').textContent = eintrag.name + ' aus ' + zielTitel({ ziel: ziel }) + ' aushängen?';
2517
+ $('dma-text').textContent = 'Der Eintrag verschwindet aus ' + mcpOrtDatei(ziel)
2518
+ + '. Der Katalog-Eintrag bleibt, du kannst ihn jederzeit wieder einhängen.';
2519
+ zeige($('d-mcpaus'));
2520
+ }
2521
+ nachDialog($('d-mcpaus'), async function (wert) {
2522
+ var kontext = mcpAusKontext;
2523
+ mcpAusKontext = null;
2524
+ if (wert !== 'ja' || !kontext) return;
2525
+ var ergebnis = await rufe(function () {
2526
+ return window.hmgr.mcpAushaengen({ name: kontext.eintrag.name, harness: kontext.ziel.harness, ort: kontext.ziel.ort });
2527
+ });
2528
+ await ladeAlles();
2529
+ var teile = [kontext.eintrag.name + ' aus ' + zielTitel({ ziel: kontext.ziel }) + ' ausgehängt.'];
2530
+ if (ergebnis.hinweis) teile.push(ergebnis.hinweis);
2531
+ melde(teile.join('\n'));
2532
+ });
2533
+
2534
+ /* ---------------- Übernehmen aus der geteilten .mcp.json (E64) ---------------- */
2535
+ /* Der einzige Weg, auf dem hmgr die geteilte Datei anfasst, deshalb eine ausdrückliche Rückfrage:
2536
+ der Eintrag wandert in die private Konfiguration und verschwindet aus der Datei — für alle, die
2537
+ sie teilen. Der Name muss nicht im Katalog stehen, übernommen wird der Block aus der Datei. */
2538
+ var mcpUebernahmeKontext = null;
2539
+ function frageMcpUebernahme(name, ziel, datei) {
2540
+ mcpUebernahmeKontext = { name: name, ziel: ziel };
2541
+ $('dmu-titel').textContent = name + ' übernehmen?';
2542
+ $('dmu-text').textContent = name + ' wird in deine private Konfiguration für '
2543
+ + zielTitel({ ziel: ziel }) + ' übernommen und aus ' + (datei || '.mcp.json')
2544
+ + ' entfernt. Ist die Datei mit dem Team geteilt, verlieren die anderen den Eintrag, sobald die '
2545
+ + 'Änderung committet ist.';
2546
+ zeige($('d-mcpueb'));
2547
+ }
2548
+ nachDialog($('d-mcpueb'), async function (wert) {
2549
+ var kontext = mcpUebernahmeKontext;
2550
+ mcpUebernahmeKontext = null;
2551
+ if (wert !== 'ja' || !kontext) return;
2552
+ var ergebnis = await rufe(function () {
2553
+ return window.hmgr.mcpUebernehmen({ name: kontext.name, harness: kontext.ziel.harness, ort: kontext.ziel.ort });
2554
+ });
2555
+ await ladeAlles();
2556
+ var teile = [kontext.name + ' gehört jetzt zu deiner privaten Konfiguration für '
2557
+ + zielTitel({ ziel: kontext.ziel }) + ' und steht nicht mehr in ' + ergebnis.datei + '.'];
2558
+ if (ergebnis.hinweis) teile.push(ergebnis.hinweis);
2559
+ melde(teile.join('\n'));
2560
+ });
2561
+
2562
+ /* ---------------- MCP hinzufügen (E61-GUI-Nachtrag) ---------------- */
2563
+ /* Gegenrichtung zu oeffneMcpEinhaengen: von der Ziel-Karte aus, Mehrfachauswahl über den ganzen
2564
+ Katalog, dasselbe Dialogmuster wie „Skills hinzufügen" (E47) statt der Ortswahl von d-mcp. */
2565
+ var mcpHinzufuegenKontext = null;
2566
+ function dmhBilanzAktualisieren() {
2567
+ var n = $('dmh-liste').querySelectorAll('input:checked').length;
2568
+ $('dmh-bilanz').textContent = zahlwort(n, 'ausgewählt', 'ausgewählt');
2569
+ }
2570
+ function oeffneMcpHinzufuegen(ziel) {
2571
+ mcpHinzufuegenKontext = { ziel: ziel };
2572
+ $('dmh-ziel').textContent = zielTitel({ ziel: ziel });
2573
+ var box = $('dmh-liste');
2574
+ box.textContent = '';
2575
+ // Ohne Katalog stünde hier ein leerer Kasten mit einem Knopf, der nichts tut: hmgr legt
2576
+ // MCP-Einträge nie selbst an (E61), also nennt der Dialog denselben Weg wie die MCP-Ansicht.
2577
+ if (MCP_KATALOG.length === 0) {
2578
+ box.appendChild(e('p', 'ohne', 'Noch kein Server im Katalog. hmgr liest ihn aus '
2579
+ + pfadSchreibweise('~/.hmgr/mcp.yaml') + ' und schreibt die Datei nie.'));
2580
+ }
2581
+ MCP_KATALOG.forEach(function (k, i) {
2582
+ var schon = k.eingehaengt.some(function (z) { return zielKey(z) === zielKey(ziel); });
2583
+ var label = e('label', 'wahlzeile');
2584
+ var input = document.createElement('input');
2585
+ input.type = 'checkbox';
2586
+ input.value = String(i);
2587
+ input.disabled = schon;
2588
+ input.addEventListener('change', dmhBilanzAktualisieren);
2589
+ label.appendChild(input);
2590
+ var txt = e('span');
2591
+ txt.appendChild(e('b', null, k.eintrag.name));
2592
+ if (k.eintrag.beschreibung) txt.appendChild(e('span', 'mono', k.eintrag.beschreibung));
2593
+ if (schon) txt.appendChild(e('span', 'notiz', 'Hängt hier schon.'));
2594
+ label.appendChild(txt);
2595
+ box.appendChild(label);
2596
+ });
2597
+ dmhBilanzAktualisieren();
2598
+ zeige($('d-mcp-hinzufuegen'));
2599
+ }
2600
+
2601
+ $('d-mcp-hinzufuegen').addEventListener('submit', function (ev) {
2602
+ if (ev.submitter && ev.submitter.value === 'los') {
2603
+ var gewaehlt = Array.from($('dmh-liste').querySelectorAll('input:checked'));
2604
+ if (gewaehlt.length === 0) ev.preventDefault();
2605
+ }
2606
+ });
2607
+
2608
+ nachDialog($('d-mcp-hinzufuegen'), async function (wert) {
2609
+ if (wert !== 'los' || mcpHinzufuegenKontext === null) return;
2610
+ var ziel = mcpHinzufuegenKontext.ziel;
2611
+ var indizes = Array.from($('dmh-liste').querySelectorAll('input:checked')).map(function (i) { return +i.value; });
2612
+ var erfolge = 0;
2613
+ var hinweise = [];
2614
+ var fehlschlaege = [];
2615
+ for (var n = 0; n < indizes.length; n++) {
2616
+ var k = MCP_KATALOG[indizes[n]];
2617
+ try {
2618
+ var ergebnis = await rufe(function () {
2619
+ return window.hmgr.mcpEinhaengen({ name: k.eintrag.name, harness: ziel.harness, ort: ziel.ort });
2620
+ });
2621
+ erfolge++;
2622
+ if (ergebnis.hinweis && hinweise.indexOf(ergebnis.hinweis) < 0) hinweise.push(ergebnis.hinweis);
2623
+ // F39-Warnungen gesammelt im Erfolgs-Toast statt je Einhängen einzeln zu melden.
2624
+ if (ergebnis.warnung) hinweise.push(k.eintrag.name + ': ' + ergebnis.warnung);
2625
+ } catch (fehler) {
2626
+ // rufe() hat den Fehler zwar schon in die Meldungsleiste gesetzt, die Schluss-Meldung unten
2627
+ // überschreibt sie aber gleich wieder. Ein Teilerfolg muss beide Seiten nennen, sonst behauptet
2628
+ // der Toast einen Lauf ohne Fehler. Die übrigen Einträge laufen weiter.
2629
+ fehlschlaege.push(k.eintrag.name + ': ' + fehler.message);
2630
+ }
2631
+ }
2632
+ ZIEL_OFFEN[kartenKey(ziel)] = true;
2633
+ await ladeAlles();
2634
+ var zeilen = [erfolge > 0
2635
+ ? zahlwort(erfolge, 'MCP-Server hängt', 'MCP-Server hängen') + ' jetzt in ' + zielTitel({ ziel: ziel }) + '.'
2636
+ : 'Nichts eingehängt in ' + zielTitel({ ziel: ziel }) + '.'];
2637
+ hinweise.forEach(function (h) { zeilen.push(h); });
2638
+ if (fehlschlaege.length > 0) {
2639
+ zeilen.push(zahlwort(fehlschlaege.length, 'Eintrag ließ', 'Einträge ließen') + ' sich nicht einhängen:');
2640
+ fehlschlaege.forEach(function (f) { zeilen.push(f); });
2641
+ }
2642
+ melde(zeilen.join('\n'));
2643
+ });
2644
+
2645
+ /* ================= EINSTELLUNGEN ================= */
2646
+
2647
+ /** Ziele eines Harness-Typs mit mindestens einem eigenen Link oder liegen gebliebener
2648
+ harness-eigener Registrierung ohne Links (E38 R7): die Konfiguration, die „Säubern" wegräumt.
2649
+ Aus istZustand abgelesen, wie GRILL.md Entwurfskritik 4 es vorgibt. */
2650
+ function harnessBilanz(h) {
2651
+ var ziele = ZIELE.filter(function (z) {
2652
+ if (z.ziel.harness !== h) return false;
2653
+ return z.eintraege.some(function (ei) { return ei.art !== 'fremd'; }) || z.istRegistriert;
2654
+ });
2655
+ var links = 0, eintraege = 0;
2656
+ ziele.forEach(function (z) {
2657
+ links += z.eintraege.filter(function (ei) { return ei.art !== 'fremd'; }).length;
2658
+ if (z.istRegistriert) eintraege += 1;
2659
+ });
2660
+ return { ziele: ziele, links: links, eintraege: eintraege };
2661
+ }
2662
+ function zahlwort(n, ein, viele) { return n + ' ' + (n === 1 ? ein : viele); }
2663
+ function bilanzText(b) {
2664
+ // Wer den harness-eigenen Eintrag angelegt hat, weiß niemand (E38 R7), also wird er benannt,
2665
+ // nicht hmgr zugeschrieben.
2666
+ var teile = [b.links
2667
+ ? zahlwort(b.links, 'Link', 'Links') + ' in ' + zahlwort(b.ziele.length, 'Ziel', 'Zielen')
2668
+ : zahlwort(b.ziele.length, 'leeres Ziel', 'leere Ziele')];
2669
+ if (b.eintraege) teile.push(zahlwort(b.eintraege, 'harness-eigener Eintrag', 'harness-eigene Einträge'));
2670
+ return teile.join(', ');
2671
+ }
2672
+ function fuelleBilanzListe(ul, bilanz) {
2673
+ ul.textContent = '';
2674
+ bilanz.ziele.forEach(function (z) {
2675
+ var linkZahl = z.eintraege.filter(function (ei) { return ei.art !== 'fremd'; }).length;
2676
+ ul.appendChild(e('li', null, pfadKurz(z.verzeichnis) + ' · ' + zahlwort(linkZahl, 'Link', 'Links')));
2677
+ });
2678
+ }
2679
+
2680
+ /** Was „Säubern" weggeräumt hat, aus dem Ergebnis des Anwendungsfalls statt aus einer Schätzung. */
2681
+ function saeuberMeldung(kopf, ergebnis) {
2682
+ var ziele = ergebnis.ziele.filter(function (z) { return z.entfernteLinks.length > 0; });
2683
+ var links = 0;
2684
+ ziele.forEach(function (z) { links += z.entfernteLinks.length; });
2685
+ var eintraege = ergebnis.ziele.filter(function (z) { return z.konfigurationZurueckgebaut; }).length;
2686
+ var teile = [];
2687
+ if (links) teile.push(zahlwort(links, 'Link', 'Links') + ' in ' + zahlwort(ziele.length, 'Ziel', 'Zielen'));
2688
+ if (eintraege) teile.push(zahlwort(eintraege, 'harness-eigener Eintrag', 'harness-eigene Einträge'));
2689
+ return kopf + ' '
2690
+ + (teile.length ? teile.join(' und ') + ' entfernt' : 'Es war nichts wegzuräumen')
2691
+ + '. Nicht von hmgr verwaltete Einträge liegen unverändert in den Verzeichnissen.';
2692
+ }
2693
+
2694
+ function baueHarnessliste() {
2695
+ var ul = $('harnessliste');
2696
+ // Der Neuaufbau wirft den Schalter weg, auf dem der Fokus steht. Nach einer Rückfrage ist genau
2697
+ // das der Fall, also merken und wieder hinsetzen.
2698
+ var vorher = document.activeElement;
2699
+ var merk = vorher && vorher.dataset && ul.contains(vorher) ? vorher.dataset.harness : null;
2700
+ ul.textContent = '';
2701
+ HARNESSE_ALLE.forEach(function (h) {
2702
+ var li = document.createElement('li');
2703
+ var label = e('label', 'harnessschalter');
2704
+ var input = document.createElement('input');
2705
+ input.type = 'checkbox';
2706
+ input.checked = harnessAktiv(h);
2707
+ input.dataset.harness = h;
2708
+ input.addEventListener('change', function () { schalteHarness(h, input.checked); });
2709
+ label.appendChild(input);
2710
+
2711
+ var txt = e('span');
2712
+ txt.appendChild(beschriftet(e('b', null, harnessName(h)), h));
2713
+ var bilanz = harnessBilanz(h);
2714
+ var stand = harnessAktiv(h)
2715
+ ? (bilanz.ziele.length
2716
+ ? zahlwort(bilanz.ziele.length, 'Ziel', 'Ziele') + ' · ' + zahlwort(bilanz.links, 'Link', 'Links')
2717
+ : 'aktiv, noch kein Ziel')
2718
+ : (bilanz.ziele.length ? 'inaktiv, Konfiguration liegt noch' : 'inaktiv');
2719
+ // Der Schalter bleibt auch ohne Installation stehen, damit Säubern erreichbar ist (E42).
2720
+ if (HARNESSE_NICHT_INSTALLIERT.indexOf(h) !== -1) stand += ' · nicht installiert';
2721
+ txt.appendChild(e('small', null, stand));
2722
+ label.appendChild(txt);
2723
+ li.appendChild(label);
2724
+ ul.appendChild(li);
2725
+ });
2726
+ if (merk) {
2727
+ var zurueck = ul.querySelector('input[data-harness="' + merk + '"]');
2728
+ if (zurueck) zurueck.focus();
2729
+ }
2730
+ }
2731
+
2732
+ var harnessKandidat = null;
2733
+ function schalteHarness(h, an) {
2734
+ var bilanz = harnessBilanz(h);
2735
+ if (an) {
2736
+ rufe(function () { return window.hmgr.harnessAktivieren(h); }).then(async function () {
2737
+ await ladeAlles();
2738
+ melde(harnessName(h) + ' ist wieder aktiv. ' + (bilanz.ziele.length
2739
+ ? 'hmgr pflegt ' + zahlwort(bilanz.ziele.length, 'vorhandenes Ziel', 'vorhandene Ziele') + ' wieder.'
2740
+ : 'Die Plätze zum Einrichten stehen wieder offen.'));
2741
+ });
2742
+ return;
2743
+ }
2744
+ if (bilanz.ziele.length === 0) {
2745
+ rufe(function () { return window.hmgr.harnessDeaktivieren(h); }).then(async function () {
2746
+ await ladeAlles();
2747
+ melde(harnessName(h) + ' ist inaktiv. hmgr hat hier keine Links liegen.');
2748
+ });
2749
+ return;
2750
+ }
2751
+ harnessKandidat = h;
2752
+ $('dh-titel').textContent = harnessName(h) + ' deaktivieren?';
2753
+ beschriftet($('dh-titel'), h);
2754
+ $('dh-text').textContent = 'Es liegt noch Konfiguration (' + bilanzText(bilanz)
2755
+ + '). Säubern entfernt die hmgr-Links und harness-eigene Einträge, nicht von hmgr verwaltete Einträge bleiben.';
2756
+ fuelleBilanzListe($('dh-liste'), bilanz);
2757
+ zeige($('d-harness'));
2758
+ }
2759
+ nachDialog($('d-harness'), async function (wert) {
2760
+ var h = harnessKandidat;
2761
+ harnessKandidat = null;
2762
+ if (!h) return;
2763
+ if (wert === 'saeubern') {
2764
+ await rufe(function () { return window.hmgr.harnessDeaktivieren(h); });
2765
+ var ergebnis = await rufe(function () { return window.hmgr.saeubern(h); });
2766
+ await ladeAlles();
2767
+ melde(saeuberMeldung(harnessName(h) + ' deaktiviert und gesäubert.', ergebnis));
2768
+ } else if (wert === 'nur') {
2769
+ var liegen = bilanzText(harnessBilanz(h));
2770
+ await rufe(function () { return window.hmgr.harnessDeaktivieren(h); });
2771
+ await ladeAlles();
2772
+ melde(harnessName(h) + ' deaktiviert. ' + liegen + ' bleiben liegen, hmgr pflegt sie nicht mehr. Im Ziel steht ein Säubern-Knopf, wenn du sie doch loswerden willst.');
2773
+ } else {
2774
+ // Abbruch: der Schalter im DOM steht noch auf aus, die Einstellung nicht. Neu aufbauen setzt ihn zurück.
2775
+ baueHarnessliste();
2776
+ }
2777
+ });
2778
+
2779
+ /* Derselbe Weg für Konfiguration, die schon ohne aktiven Harness dalag (Säubern-Knopf im Ziel). */
2780
+ var saeuberKandidat = null;
2781
+ function frageSaeubern(h) {
2782
+ saeuberKandidat = h;
2783
+ var bilanz = harnessBilanz(h);
2784
+ $('ds-titel').textContent = harnessName(h) + ' säubern?';
2785
+ beschriftet($('ds-titel'), h);
2786
+ $('ds-text').textContent = harnessName(h) + ' ist inaktiv, Konfiguration liegt noch (' + bilanzText(bilanz)
2787
+ + '). Säubern entfernt die hmgr-Links und harness-eigene Einträge, nicht von hmgr verwaltete Einträge bleiben.';
2788
+ fuelleBilanzListe($('ds-liste'), bilanz);
2789
+ zeige($('d-saeubern'));
2790
+ }
2791
+ nachDialog($('d-saeubern'), async function (wert) {
2792
+ var h = saeuberKandidat;
2793
+ saeuberKandidat = null;
2794
+ if (wert !== 'los' || !h) return;
2795
+ var ergebnis = await rufe(function () { return window.hmgr.saeubern(h); });
2796
+ await ladeAlles();
2797
+ melde(saeuberMeldung(harnessName(h) + ' gesäubert.', ergebnis));
2798
+ });
2799
+
2800
+ function baueEinstellungen() {
2801
+ baueHarnessliste();
2802
+ var liste = $('quellenliste');
2803
+ liste.textContent = '';
2804
+ if (QUELLEN.length === 0) {
2805
+ liste.appendChild(e('li', null, 'Noch keine Quelle. „Quelle hinzufügen …" legt die erste an.'));
2806
+ return;
2807
+ }
2808
+ QUELLEN.forEach(function (q) {
2809
+ var li = document.createElement('li');
2810
+ li.appendChild(repoIcon(q));
2811
+ var beschriftung = e('b', null, q.name);
2812
+ beschriftung.title = q.text;
2813
+ li.appendChild(beschriftung);
2814
+
2815
+ // Zweite Zeile, zurückgenommen: volle Adresse, Ref, Skill-Zahl (E60). Ref und Zahl kommen aus
2816
+ // dem schon geladenen Katalog (SKILLS), eine lokale Quelle hat keinen Ref.
2817
+ var ref = STANDARD_REF[q.text];
2818
+ var zahl = SKILLS.filter(function (s) { return s.quelleText === q.text; }).length;
2819
+ var teile = [q.text, q.art === 'lokal' ? 'Arbeitskopie' : (ref || '–'), zahlwort(zahl, 'Skill', 'Skills')];
2820
+ li.appendChild(e('small', null, teile.join(' · ')));
2821
+
2822
+ // Beide Knöpfe in einem Kasten: die Zeile ist ein Grid mit drei Spalten, zwei einzelne
2823
+ // Knöpfe lägen sonst beide in derselben Zelle (Spalte 3, Zeile 1) übereinander.
2824
+ var knoepfe = e('span', 'quellenknoepfe');
2825
+ var stift = e('button', 'umbenennen');
2826
+ stift.type = 'button';
2827
+ stift.title = 'Namen ändern';
2828
+ stift.setAttribute('aria-label', q.name + ' umbenennen');
2829
+ var stiftSvg = document.createElementNS('http://www.w3.org/2000/svg', 'svg');
2830
+ stiftSvg.setAttribute('viewBox', '0 0 16 16');
2831
+ stiftSvg.setAttribute('fill', 'none');
2832
+ stiftSvg.setAttribute('stroke', 'currentColor');
2833
+ stiftSvg.setAttribute('stroke-width', '1.5');
2834
+ stiftSvg.setAttribute('stroke-linejoin', 'round');
2835
+ stiftSvg.setAttribute('aria-hidden', 'true');
2836
+ stiftSvg.innerHTML = '<path d="M2.6 13.4v-2.5l8-8 2.5 2.5-8 8z"/><path d="M8.9 4.6 11.4 7.1"/>';
2837
+ stift.appendChild(stiftSvg);
2838
+ stift.addEventListener('click', function () { oeffneQuelleUmbenennen(q); });
2839
+ knoepfe.appendChild(stift);
2840
+ var weg = e('button', 'knopf leise klein', 'Entfernen …');
2841
+ weg.type = 'button';
2842
+ weg.setAttribute('aria-label', q.name + ' entfernen');
2843
+ weg.addEventListener('click', function () { frageQuelleVergessen(q); });
2844
+ knoepfe.appendChild(weg);
2845
+ li.appendChild(knoepfe);
2846
+ liste.appendChild(li);
2847
+ });
2848
+ }
2849
+ $('einstellungen').addEventListener('click', function () { zeige($('d-einstellungen')); });
2850
+
2851
+ /* ---------------- Quelle hinzufügen (E54, E59) ---------------- */
2852
+
2853
+ // Vorschlag für den Anzeigenamen, aus der getippten URL/dem Pfad abgeleitet (E59), so lange der
2854
+ // Nutzer das Namensfeld nicht selbst angefasst hat (dqNameBeruehrt). Kleine Kopie derselben Regel
2855
+ // wie quelleAnzeigename/quelleNameAbleiten im Kern: letztes Segment ohne .git-Suffix, dazu die
2856
+ // SSH-Kurzschreibweise git@host:pfad, deren Trenner ein Doppelpunkt statt eines Schrägstrichs ist.
2857
+ function vorschlagAusText(text) {
2858
+ var t = text.trim();
2859
+ if (!t) return '';
2860
+ var scp = /^[^@/]+@[^@/:]+:(.+)$/.exec(t);
2861
+ var pfadteil = scp ? scp[1] : t;
2862
+ var ohneSchraeg = pfadteil.replace(/\/+$/, '');
2863
+ var ohneGit = ohneSchraeg.endsWith('.git') ? ohneSchraeg.slice(0, -4) : ohneSchraeg;
2864
+ return letztesSegment(ohneGit);
2865
+ }
2866
+ var dqNameBeruehrt = false;
2867
+ $('dq-name').addEventListener('input', function () { dqNameBeruehrt = true; });
2868
+ $('dq-text').addEventListener('input', function () {
2869
+ if (dqNameBeruehrt) return;
2870
+ $('dq-name').value = vorschlagAusText($('dq-text').value);
2871
+ });
2872
+
2873
+ $('quelleneu').addEventListener('click', function () {
2874
+ $('dq-text').value = '';
2875
+ $('dq-name').value = '';
2876
+ dqNameBeruehrt = false;
2877
+ $('dq-fehler').hidden = true;
2878
+ zeige($('d-quelle'));
2879
+ });
2880
+ enterBestaetigt($('dq-text'), $('dq-los'));
2881
+ enterBestaetigt($('dq-name'), $('dq-los'));
2882
+
2883
+ // Kein nachDialog: bei einem Fehler (Quelle nicht erreichbar) muss der Dialog offen bleiben und
2884
+ // den Fehler zeigen, statt sich wie bei Erfolg zu schließen. Deshalb der IPC-Aufruf direkt hier,
2885
+ // ohne rufe() (das würde immer die Meldungsleiste befüllen und werfen).
2886
+ $('d-quelle').addEventListener('submit', function (ev) {
2887
+ if (!ev.submitter || ev.submitter.value !== 'los') return;
2888
+ ev.preventDefault();
2889
+ quelleHinzufuegenLos();
2890
+ });
2891
+ // Das Hinzufügen holt die Quelle aus dem Netz, das dauert. Ohne Sperre legte ein zweiter Anstoß
2892
+ // in der Zwischenzeit einen zweiten Lauf an, der denselben Zustand liest und schreibt: die Quelle
2893
+ // stünde zweimal im Zustand. Sperre über den disabled-Zustand wie beim Aktualisieren-Knopf.
2894
+ async function quelleHinzufuegenLos() {
2895
+ var knopf = $('dq-los');
2896
+ if (knopf.disabled) return;
2897
+ var text = $('dq-text').value.trim();
2898
+ var name = $('dq-name').value.trim();
2899
+ $('dq-fehler').hidden = true;
2900
+ if (!text) return;
2901
+ knopf.disabled = true;
2902
+ knopf.textContent = 'Läuft …';
2903
+ var ergebnis;
2904
+ try {
2905
+ ergebnis = await window.hmgr.quelleHinzufuegen(text, name || undefined);
2906
+ } finally {
2907
+ knopf.disabled = false;
2908
+ knopf.textContent = 'Hinzufügen';
2909
+ }
2910
+ if (!ergebnis.ok) {
2911
+ $('dq-fehler').textContent = ergebnis.fehler;
2912
+ $('dq-fehler').hidden = false;
2913
+ return;
2914
+ }
2915
+ $('d-quelle').close('los');
2916
+ var daten = ergebnis.daten;
2917
+ await ladeAlles();
2918
+ if (daten.bekannt) {
2919
+ // Die Adresse gehört dazu (E54): gemeldet wird, unter welcher Schreibweise die Quelle geführt
2920
+ // wird, der Anzeigename allein sagt das nicht.
2921
+ melde('Quelle ist schon bekannt, geführt als „' + daten.quelle.name + '“ (' + daten.quelle.text + ').');
2922
+ return;
2923
+ }
2924
+ var teile = [zahlwort(daten.skills.length, 'Skill', 'Skills')];
2925
+ if (daten.marketplace) teile.push('Marketplace „' + daten.marketplace.name + '“');
2926
+ melde('Quelle „' + daten.quelle.name + '“ hinzugefügt, ' + teile.join(', ') + '.');
2927
+ }
2928
+
2929
+ /* ---------------- Quelle umbenennen (E59) ---------------- */
2930
+
2931
+ var quelleUmbenennenKontext = null;
2932
+ function oeffneQuelleUmbenennen(quelle) {
2933
+ quelleUmbenennenKontext = quelle;
2934
+ $('dqu-text').textContent = '„' + quelle.name + '“ (' + quelle.text + ')';
2935
+ $('dqu-name').value = quelle.name;
2936
+ $('dqu-fehler').hidden = true;
2937
+ zeige($('d-quelle-umbenennen'));
2938
+ }
2939
+ enterBestaetigt($('dqu-name'), $('dqu-los'));
2940
+
2941
+ // Wie bei d-quelle: kein nachDialog, ein Fehler (Namenskollision) muss den Dialog offen halten
2942
+ // und den Fehler dort zeigen (E59), statt sich wie bei Erfolg zu schließen.
2943
+ $('d-quelle-umbenennen').addEventListener('submit', function (ev) {
2944
+ if (!ev.submitter || ev.submitter.value !== 'los') return;
2945
+ ev.preventDefault();
2946
+ quelleUmbenennenLos();
2947
+ });
2948
+ async function quelleUmbenennenLos() {
2949
+ var knopf = $('dqu-los');
2950
+ if (knopf.disabled || quelleUmbenennenKontext === null) return;
2951
+ var quelle = quelleUmbenennenKontext;
2952
+ var name = $('dqu-name').value.trim();
2953
+ $('dqu-fehler').hidden = true;
2954
+ if (!name) return;
2955
+ knopf.disabled = true;
2956
+ knopf.textContent = 'Läuft …';
2957
+ var ergebnis;
2958
+ try {
2959
+ ergebnis = await window.hmgr.quelleUmbenennen(quelle.text, name);
2960
+ } finally {
2961
+ knopf.disabled = false;
2962
+ knopf.textContent = 'Umbenennen';
2963
+ }
2964
+ if (!ergebnis.ok) {
2965
+ $('dqu-fehler').textContent = ergebnis.fehler;
2966
+ $('dqu-fehler').hidden = false;
2967
+ return;
2968
+ }
2969
+ quelleUmbenennenKontext = null;
2970
+ $('d-quelle-umbenennen').close('los');
2971
+ await ladeAlles();
2972
+ melde('Quelle heißt jetzt „' + ergebnis.daten.name + '“.');
2973
+ }
2974
+
2975
+ /* ---------------- Quelle entfernen (E54) ---------------- */
2976
+
2977
+ var quelleVergessenKontext = null;
2978
+ /** Zahl der Links, die nach dem Entfernen als „Link ohne Quelle" liegen bleiben. Über alle Ziele,
2979
+ auch die von abgeschalteten Harnesses und aus Arbeitsverzeichnissen. Die Quelle eines Links
2980
+ ist derselbe Zustandseintrag, den die Quellenliste zeigt (der Adapter setzt sie von dort ein),
2981
+ also ist der Textvergleich hier die Identität, keine Anzeige-Näherung. */
2982
+ function betroffeneLinksZahl(quelle) {
2983
+ var n = 0;
2984
+ ZIELE.forEach(function (z) {
2985
+ z.eintraege.forEach(function (ei) {
2986
+ if (ei.art !== 'link') return;
2987
+ var q = ei.link.checkout.quelle;
2988
+ var textq = q.art === 'git' ? q.url : q.pfad;
2989
+ if (q.art === quelle.art && textq === quelle.text) n++;
2990
+ });
2991
+ });
2992
+ return n;
2993
+ }
2994
+ function frageQuelleVergessen(quelle) {
2995
+ quelleVergessenKontext = quelle;
2996
+ var n = betroffeneLinksZahl(quelle);
2997
+ $('dqv-titel').textContent = '„' + quelle.name + '“ entfernen?';
2998
+ $('dqv-text').textContent = 'Checkouts und Links bleiben liegen. '
2999
+ + (n === 0
3000
+ ? 'Es gibt aktuell keine Links dazu.'
3001
+ : zahlwort(n, 'Link gilt', 'Links gelten') + ' danach als „Link ohne Quelle“, hmgr aktualisiert '
3002
+ + (n === 1 ? 'ihn' : 'sie') + ' nicht mehr. Wieder hinzufügen macht '
3003
+ + (n === 1 ? 'ihn' : 'sie') + ' wieder zu ' + (n === 1 ? 'einem normalen Link' : 'normalen Links') + '.');
3004
+ zeige($('d-quelle-vergessen'));
3005
+ }
3006
+ /** Nach dem „entfernen?“ vergisst hmgr die Quelle, Checkouts und Links bleiben liegen (E54). */
3007
+ async function quelleVergessen(quelle) {
3008
+ await rufe(function () { return window.hmgr.quelleEntfernen(quelle.text); });
3009
+ await ladeAlles();
3010
+ // Der Neuaufbau der Liste wirft den Knopf weg, auf dem der Fokus nach dem Dialog steht. Der
3011
+ // nächstliegende Platz ist der Knopf, der eine neue Quelle anlegt; er steht fest im Markup.
3012
+ $('quelleneu').focus();
3013
+ melde('Quelle „' + quelle.name + '“ aus hmgr entfernt.');
3014
+ }
3015
+ nachDialog($('d-quelle-vergessen'), async function (wert) {
3016
+ if (wert !== 'ja' || quelleVergessenKontext === null) return;
3017
+ var quelle = quelleVergessenKontext;
3018
+ quelleVergessenKontext = null;
3019
+ // Inventar (E58) VOR jeder Wirkung erheben: quelleEntfernen macht ihre Links zu „Link ohne
3020
+ // Quelle“, danach ist die Zuordnung weg. Bei leerem Inventar keine Rückfrage (F31a): sofort
3021
+ // vergessen wie bisher.
3022
+ var inventar = await rufe(function () { return window.hmgr.quelleAufraeumInventar(quelle.text); });
3023
+ if (inventar.links.length === 0 && inventar.checkouts.length === 0) {
3024
+ await quelleVergessen(quelle);
3025
+ return;
3026
+ }
3027
+ frageQuelleAufraeumen(quelle, inventar);
3028
+ });
3029
+
3030
+ /* ---------------- Skills der Quelle komplett löschen (E58, F31a) ---------------- */
3031
+
3032
+ var quelleAufraeumenKontext = null;
3033
+ function frageQuelleAufraeumen(quelle, inventar) {
3034
+ quelleAufraeumenKontext = { quelle: quelle, inventar: inventar };
3035
+ var teile = [
3036
+ zahlwort(inventar.links.length, 'Link', 'Links'),
3037
+ zahlwort(inventar.checkouts.length, 'Checkout', 'Checkouts'),
3038
+ ];
3039
+ function bleibt(n) { return n === 1 ? ', bleibt liegen' : ', bleiben liegen'; }
3040
+ if (inventar.plugins.length > 0) {
3041
+ teile.push(zahlwort(inventar.plugins.length, 'installiertes Plugin', 'installierte Plugins') + bleibt(inventar.plugins.length));
3042
+ }
3043
+ if (inventar.zeigerPfade.length > 0) {
3044
+ teile.push(zahlwort(inventar.zeigerPfade.length, 'Zeiger-Eintrag', 'Zeiger-Einträge') + bleibt(inventar.zeigerPfade.length));
3045
+ }
3046
+ var text = 'Skills von „' + quelle.name + '“ komplett vom Rechner löschen (' + teile.join(', ') + ')?';
3047
+ // Warnung bei lokalen Änderungen (E15, F31a): ein Ja bricht dann ohne zu löschen ab, auch die
3048
+ // Quelle bleibt bekannt, sichtbar VOR der Entscheidung, damit „Behalten“ die naheliegende Wahl ist.
3049
+ var lok = inventar.checkoutsMitLokalenAenderungen || [];
3050
+ if (lok.length > 0) {
3051
+ text += ' Achtung: ' + zahlwort(lok.length, 'Checkout hat', 'Checkouts haben') + ' lokale Änderungen: '
3052
+ + lok.map(function (c) { return pfadKurz(c.pfad); }).join(', ')
3053
+ + '. Ein Ja bricht dann ab, ohne zu löschen, die Quelle bleibt bekannt.';
3054
+ }
3055
+ $('dqa-text').textContent = text;
3056
+ // Erst nach einer kurzen Pause, wie beim Skill-Ordner-Lösch-Angebot (E56): zwei <dialog>
3057
+ // lassen sich nicht im selben close-Ereignis nacheinander öffnen.
3058
+ setTimeout(function () { zeige($('d-quelle-aufraeumen')); }, 90);
3059
+ }
3060
+ nachDialog($('d-quelle-aufraeumen'), async function (wert) {
3061
+ var kontext = quelleAufraeumenKontext;
3062
+ quelleAufraeumenKontext = null;
3063
+ if (kontext === null) return;
3064
+ // Nein: wie bisher vergessen, alles bleibt liegen (E54).
3065
+ if (wert !== 'loeschen') {
3066
+ await quelleVergessen(kontext.quelle);
3067
+ return;
3068
+ }
3069
+ // Ja: erst aufräumen. Bei lokalen Änderungen bricht quelleAufraeumen ab (F31a), dann bleibt die
3070
+ // Quelle bekannt und nichts wird gelöscht, nicht einmal vergessen.
3071
+ var ergebnis;
3072
+ try {
3073
+ ergebnis = await rufe(function () { return window.hmgr.quelleAufraeumen(kontext.inventar); });
3074
+ } catch (fehler) {
3075
+ await ladeAlles();
3076
+ return;
3077
+ }
3078
+ await rufe(function () { return window.hmgr.quelleEntfernen(kontext.quelle.text); });
3079
+ await ladeAlles();
3080
+ $('quelleneu').focus();
3081
+ melde(
3082
+ zahlwort(ergebnis.entfernteLinks, 'Link', 'Links') + ' und '
3083
+ + zahlwort(ergebnis.entfernteCheckouts, 'Checkout', 'Checkouts') + ' von „' + kontext.quelle.name + '“ gelöscht, die Quelle ist vergessen.',
3084
+ );
3085
+ });
3086
+
3087
+ /* ---------------- Skill-Ordner festlegen (E56) ---------------- */
3088
+
3089
+ var skillOrdnerZiel = null;
3090
+ function oeffneSkillOrdnerFestlegen(z) {
3091
+ skillOrdnerZiel = z;
3092
+ $('dso-pfad').value = '';
3093
+ $('dso-fehler').hidden = true;
3094
+ zeige($('d-skillordner'));
3095
+ }
3096
+ enterBestaetigt($('dso-pfad'), $('dso-los'));
3097
+
3098
+ // Wie bei d-quelle: bei einem Fehler (Pfad ungültig, schon verwaltet) muss der Dialog offen
3099
+ // bleiben und den Fehler zeigen, statt sich wie bei Erfolg zu schließen.
3100
+ $('d-skillordner').addEventListener('submit', function (ev) {
3101
+ if (!ev.submitter || ev.submitter.value !== 'los') return;
3102
+ ev.preventDefault();
3103
+ skillOrdnerFestlegenLos();
3104
+ });
3105
+ async function skillOrdnerFestlegenLos() {
3106
+ var knopf = $('dso-los');
3107
+ if (knopf.disabled || skillOrdnerZiel === null) return;
3108
+ var pfad = $('dso-pfad').value.trim();
3109
+ $('dso-fehler').hidden = true;
3110
+ if (!pfad) return;
3111
+ var z = skillOrdnerZiel;
3112
+ knopf.disabled = true;
3113
+ knopf.textContent = 'Läuft …';
3114
+ var ergebnis;
3115
+ try {
3116
+ ergebnis = await window.hmgr.skillOrdnerFestlegen({ harness: z.ziel.harness, ort: z.ziel.ort, pfad: pfad });
3117
+ } finally {
3118
+ knopf.disabled = false;
3119
+ knopf.textContent = 'Festlegen';
3120
+ }
3121
+ if (!ergebnis.ok) {
3122
+ $('dso-fehler').textContent = ergebnis.fehler;
3123
+ $('dso-fehler').hidden = false;
3124
+ return;
3125
+ }
3126
+ $('d-skillordner').close('los');
3127
+ skillOrdnerZiel = null;
3128
+ await ladeAlles();
3129
+ melde('Liest Skills jetzt aus ' + pfadKurz(ergebnis.daten) + '.');
3130
+ }
3131
+
3132
+ /* ---------------- Skill-Ordner entfernen und löschen (E56) ---------------- */
3133
+
3134
+ var skillOrdnerEntfernenZiel = null;
3135
+ function frageSkillOrdnerEntfernen(z) {
3136
+ skillOrdnerEntfernenZiel = z;
3137
+ $('dsoe-titel').textContent = pfadKurz(z.verwalteterSkillOrdner) + ' nicht mehr verwalten?';
3138
+ $('dsoe-text').textContent = 'Der Eintrag in Hermes\' config.yaml verschwindet, der Ordner bleibt zunächst '
3139
+ + 'erhalten. Danach fragt hmgr, ob er samt Inhalt gelöscht werden soll.';
3140
+ zeige($('d-skillordner-entfernen'));
3141
+ }
3142
+ nachDialog($('d-skillordner-entfernen'), async function (wert) {
3143
+ if (wert !== 'ja' || skillOrdnerEntfernenZiel === null) return;
3144
+ var z = skillOrdnerEntfernenZiel;
3145
+ skillOrdnerEntfernenZiel = null;
3146
+ var ergebnis = await rufe(function () {
3147
+ return window.hmgr.skillOrdnerEntfernen({ harness: z.ziel.harness, ort: z.ziel.ort });
3148
+ });
3149
+ await ladeAlles();
3150
+ melde('Skill-Ordner-Eintrag für ' + zielTitel(z) + ' entfernt.');
3151
+ skillOrdnerLoeschenKontext = { ziel: z.ziel, pfad: ergebnis.pfad };
3152
+ $('dsol-text').textContent = 'Ordner ' + pfadKurz(ergebnis.pfad) + ' enthält noch '
3153
+ + zahlwort(ergebnis.hmgrLinks, 'hmgr-Link', 'hmgr-Links') + ' und '
3154
+ + zahlwort(ergebnis.fremdeEintraege, 'fremden Eintrag', 'fremde Einträge') + '.';
3155
+ setTimeout(function () { zeige($('d-skillordner-loeschen')); }, 90);
3156
+ });
3157
+
3158
+ var skillOrdnerLoeschenKontext = null;
3159
+ nachDialog($('d-skillordner-loeschen'), async function (wert) {
3160
+ var kontext = skillOrdnerLoeschenKontext;
3161
+ skillOrdnerLoeschenKontext = null;
3162
+ if (wert !== 'loeschen' || kontext === null) return;
3163
+ await rufe(function () {
3164
+ return window.hmgr.skillOrdnerLoeschen({ harness: kontext.ziel.harness, ort: kontext.ziel.ort, pfad: kontext.pfad });
3165
+ });
3166
+ await ladeAlles();
3167
+ melde('Ordner ' + pfadKurz(kontext.pfad) + ' gelöscht.');
3168
+ });
3169
+
3170
+ /* ================= RÜCKFRAGE VOM MAIN-PROZESS (E28, E34) ================= */
3171
+
3172
+ // Die Fragen kommen fertig formuliert aus dem Kern; für die Anzeige werden Home-Pfade darin
3173
+ // wie überall sonst auf ~ gekürzt (nur Anzeige, die Antwort transportiert keinen Text zurück).
3174
+ /** Wie pfadKurz, aber mitten im Fließtext einer Rückfrage — und ebenso mit beiden Trennern (E68). */
3175
+ function frageKurz(text) {
3176
+ if (!HOME) return text;
3177
+ return text.split(HOME + '/').join('~/').split(HOME + '\\').join('~\\');
3178
+ }
3179
+ window.hmgr.onRueckfrage(function (nachricht) {
3180
+ if (nachricht.art === 'bestaetigen') {
3181
+ $('db-frage').textContent = frageKurz(nachricht.frage);
3182
+ zeige($('d-bestaetigen'));
3183
+ $('d-bestaetigen').addEventListener(
3184
+ 'close',
3185
+ function () { window.hmgr.antwortenRueckfrage(nachricht.id, $('d-bestaetigen').returnValue === 'ja'); },
3186
+ { once: true },
3187
+ );
3188
+ return;
3189
+ }
3190
+ $('da-frage').textContent = frageKurz(nachricht.frage);
3191
+ var box = $('da-optionen');
3192
+ box.textContent = '';
3193
+ (nachricht.optionen || []).forEach(function (option, i) {
3194
+ var label = e('label', 'wahlzeile');
3195
+ label.appendChild(e('span', 'nummer', String(i + 1)));
3196
+ var innen = e('span');
3197
+ var input = document.createElement('input');
3198
+ input.type = 'radio';
3199
+ input.name = 'auswahl-' + nachricht.id;
3200
+ input.value = String(i);
3201
+ input.className = 'versteckt';
3202
+ if (i === 0) input.checked = true;
3203
+ innen.appendChild(input);
3204
+ innen.appendChild(e('b', null, option));
3205
+ label.appendChild(innen);
3206
+ box.appendChild(label);
3207
+ });
3208
+ zeige($('d-auswahl'));
3209
+ $('d-auswahl').addEventListener(
3210
+ 'close',
3211
+ function () {
3212
+ var wert = $('d-auswahl').returnValue;
3213
+ if (wert !== 'weiter') {
3214
+ window.hmgr.antwortenRueckfrage(nachricht.id, undefined);
3215
+ return;
3216
+ }
3217
+ var gewaehlt = box.querySelector('input:checked');
3218
+ window.hmgr.antwortenRueckfrage(nachricht.id, gewaehlt ? +gewaehlt.value : undefined);
3219
+ },
3220
+ { once: true },
3221
+ );
3222
+ });
3223
+
3224
+ /* ---------------- Start ---------------- */
3225
+
3226
+ ladeUmgebung().then(ladeAlles).catch(function (fehler) { melde(fehler.message || String(fehler)); });
3227
+ })();