hmgr 0.0.7 → 0.0.12

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.
@@ -20,6 +20,9 @@
20
20
  // Pfade, die hier angezeigt werden, baut der Kern mit `node:path`, also mit dem Trenner der
21
21
  // Plattform — auch dann, wenn HOME selbst in POSIX-Schreibweise gesetzt ist (MSYS/Git-Bash).
22
22
  var TRENNER = '/';
23
+ // Die Plattform selbst (E78): entscheidet, ob der Weg zu einem Homedir in WSL überhaupt im Bild
24
+ // steht. Die Grenze zieht der Kern; hier soll nur kein Knopf stehen, der sicher abbricht.
25
+ var PLATTFORM = '';
23
26
  /** Trenner einer Plattform, wie `process.platform` sie nennt. Rein, nur für die Anzeige. */
24
27
  function plattformTrenner(plattform) { return plattform === 'win32' ? '\\' : '/'; }
25
28
  /**
@@ -163,15 +166,20 @@
163
166
 
164
167
  /* ---------------- Ort- und Grund-Texte ---------------- */
165
168
 
169
+ /* Ein zusätzliches Homedir (E78) heißt nach seiner Adresse, damit die Karten „claude-code · Home"
170
+ und „claude-code · wsl:Ubuntu" im Bild auseinanderzuhalten sind. */
166
171
  function ortName(ort) {
167
- if (ort.art === 'home') return 'Home';
172
+ if (ort.art === 'home') return ort.heim ? ort.heim : 'Home';
168
173
  if (ort.art === 'profil') return 'Profil „' + ort.name + '“';
169
174
  return 'Arbeitsverzeichnis ' + pfadKurz(ort.pfad);
170
175
  }
171
176
  function zielTitel(z) { return harnessName(z.ziel.harness) + ' · ' + ortName(z.ziel.ort); }
177
+ /* Gleichlautend mit zielSchluessel im Kern: ein Ort ohne heim behält seinen bisherigen Schlüssel. */
172
178
  function zielKey(ziel) {
173
179
  var o = ziel.ort;
174
- var ort = o.art === 'home' ? 'home' : o.art === 'profil' ? 'profil:' + o.name : 'dir:' + o.pfad;
180
+ var ort = o.art === 'home'
181
+ ? (o.heim ? 'home:' + o.heim : 'home')
182
+ : o.art === 'profil' ? 'profil:' + o.name : 'dir:' + o.pfad;
175
183
  return ziel.harness + '/' + ort;
176
184
  }
177
185
  function quelleKurz(quelle) { return quelle.art === 'git' ? quelle.url : quelle.pfad; }
@@ -312,6 +320,9 @@
312
320
  var HARNESSE_ALLE = [];
313
321
  var AKTIVE_HARNESSE = [];
314
322
  var HARNESSE_NICHT_INSTALLIERT = [];
323
+ // Homedirs je Harness (E78): je Eintrag { harness, heime: [{ adresse?, pfad, eingestellt }],
324
+ // kannZusaetzliche }. Grundlage der Homedir-Zeilen in den Einstellungen.
325
+ var HARNESS_HEIME = [];
315
326
  // MCP-Katalog (E61-Folgerunde): je Eintrag { eintrag, eingehaengt, fehlendeVariablen }, dazu
316
327
  // Einträge in Harness-Konfigurationen, die nicht im Katalog stehen (nur genannt). Seit F46 nur
317
328
  // noch Grundlage für den MCP-Abschnitt der Ziel-Karten (mcpZeilenFuerZiel), die MCP-Ansicht selbst
@@ -326,6 +337,14 @@
326
337
  // Ziel-Karten — nie am Harness-Namen entschieden, sondern an dieser Liste.
327
338
  var MCP_PLAETZE = [];
328
339
  var quellenFilter = 'alle';
340
+ // Sortierung der Skills-Rubrik (E79, Richtung E81): hält für die Sitzung wie quellenFilter,
341
+ // übersteht also ladeAlles. SKILLS selbst bleibt unsortiert nach Datum, damit li.dataset.index
342
+ // stabil bleibt.
343
+ var skillSortierung = 'name';
344
+ // Sortierrichtung je Schlüssel (E81): 'auf' oder 'ab'. Ein Klick auf den bereits aktiven Chip
345
+ // kehrt sie um, ein Klick auf den anderen Chip setzt die Standardrichtung des neuen Schlüssels.
346
+ var skillRichtung = 'auf';
347
+ var SKILL_RICHTUNG_STANDARD = { name: 'auf', datum: 'ab' };
329
348
  var zielFilter = 'alle';
330
349
  // Standard-Ref je Quelle (E60), aus dem Katalog abgeleitet (SKILLS trägt ihn schon je Skill): ein
331
350
  // Ref an einem Link erscheint nur, wenn er davon abweicht (`@v2.1`). Schlüssel wie quelleKurz.
@@ -335,19 +354,43 @@
335
354
 
336
355
  // Ignorierte fremde Einträge zählen hier nicht mit (E43): Ignorieren blendet die Warnung aus,
337
356
  // sonst bliebe das Ziel im Filter „braucht Aufmerksamkeit" stehen.
338
- function zielBraucht(z) {
357
+ function zielAusloeser(z) {
339
358
  var ignoriert = z.ignoriert || [];
340
- return z.eintraege.some(function (ei) {
359
+ return z.eintraege.filter(function (ei) {
341
360
  if (ei.art === 'fremd') return ignoriert.indexOf(ei.name) === -1;
342
361
  return ei.art === 'link-ohne-quelle';
343
- });
362
+ }).length;
363
+ }
364
+
365
+ // Warnung und Warnzahl (E75) sind dieselbe Regel, einmal geschrieben: die Warnung steht genau
366
+ // dann, wenn die Zählung etwas findet — sonst könnten Dreieck und Zahl auseinanderlaufen.
367
+ function zielBraucht(z) { return zielAusloeser(z) > 0; }
368
+
369
+ /**
370
+ * Warnzeichen für den Kartenkopf, mit der Zahl der Auslöser daneben (E75): eine Karte mit
371
+ * „0 Skills" und fünfzehn fremden Einträgen wirkte sonst leer, obwohl der Ordner voll ist. Die
372
+ * E71-Zahl (eigene Skills) bleibt daneben stehen, die Warnzahl ist eine eigene Information.
373
+ * Zeichen statt Text-Badge: trägt die Farbe selbst (Emoji-Darstellung), aria-label hält den
374
+ * Akkordeon-Knopf-Namen brauchbar (role="img", die Bedeutung steht nicht im Glyph selbst) und
375
+ * nennt die Zahl mit, sonst hörte sie im Screenreader gar nicht.
376
+ */
377
+ function warnzeichen(anzahl) {
378
+ var span = e('span', 'warnung');
379
+ span.setAttribute('role', 'img');
380
+ span.setAttribute(
381
+ 'aria-label',
382
+ anzahl === 1 ? '1 Eintrag braucht Aufmerksamkeit' : anzahl + ' Einträge brauchen Aufmerksamkeit',
383
+ );
384
+ span.appendChild(document.createTextNode('⚠️'));
385
+ span.appendChild(e('span', 'warnzahl', String(anzahl)));
386
+ return span;
344
387
  }
345
388
 
346
- // E45: ein Ziel eines inaktiven Harness verschwindet, sobald dort nichts liegt, weder eigene noch
347
- // fremde Einträge noch eine harness-eigene Registrierung. Ein aktiver Harness zeigt sein Ziel immer.
389
+ // E74 (revidiert E45): ein Ziel eines inaktiven Harness erscheint gar nicht, auch wenn dort noch
390
+ // Links, fremde Einträge oder eine harness-eigene Registrierung liegen — was hmgr nicht managed,
391
+ // zeigt es nicht. Beim Aktivieren taucht das Ziel samt allem, was liegen blieb, wieder auf.
348
392
  function zielSichtbar(z) {
349
- if (harnessAktiv(z.ziel.harness)) return true;
350
- return z.eintraege.length > 0 || z.istRegistriert;
393
+ return harnessAktiv(z.ziel.harness);
351
394
  }
352
395
 
353
396
  // Von Hand aufgeklappte Ziele bleiben über Neuaufbauten der Liste offen (E45), Schlüssel wie
@@ -391,6 +434,7 @@
391
434
  rufe(function () { return window.hmgr.plattform(); }),
392
435
  ]);
393
436
  HOME = werte[0];
437
+ PLATTFORM = werte[1];
394
438
  TRENNER = plattformTrenner(werte[1]);
395
439
  $('mcpkatalogpfad').textContent = pfadSchreibweise('~/.hmgr/mcp.yaml');
396
440
  }
@@ -410,6 +454,7 @@
410
454
  rufe(function () { return window.hmgr.mcpUebersicht(); }),
411
455
  rufe(function () { return window.hmgr.mcpPlaetze(); }),
412
456
  rufe(function () { return window.hmgr.bundles(); }),
457
+ rufe(function () { return window.hmgr.harnessHeime(); }),
413
458
  ]);
414
459
  ZIELE = ergebnisse[0];
415
460
  SKILLS = ergebnisse[1];
@@ -427,6 +472,7 @@
427
472
  BUNDLES = ergebnisse[9].bundles;
428
473
  BUNDLES.sort(function (a, b) { return a.name.localeCompare(b.name, 'de') || a.quelleText.localeCompare(b.quelleText, 'de'); });
429
474
  BUNDLES_KAPUTT = ergebnisse[9].kaputte;
475
+ HARNESS_HEIME = ergebnisse[10] || [];
430
476
  STANDARD_REF = {};
431
477
  SKILLS.forEach(function (s) { if (STANDARD_REF[s.quelleText] === undefined) STANDARD_REF[s.quelleText] = s.ref; });
432
478
 
@@ -493,8 +539,12 @@
493
539
  else a.removeAttribute('aria-current');
494
540
  });
495
541
  ANSICHTEN.forEach(function (x) { $('ansicht-' + x).hidden = x !== name; });
496
- // Der Bericht ist jetzt sichtbar, die Ankündigung „mit Neuem" hat ihren Dienst getan.
497
- if (name === 'update' && standZeit !== null) standRuhig();
542
+ if (name === 'update') {
543
+ // Die Ansicht ist jetzt offen, die Ankündigung „mit Neuem" hat ihren Dienst getan (E80).
544
+ if (standNeu !== null) { standNeu = null; standText(); }
545
+ // Der Verlauf zeigt immer den aktuellen Stand der Datei, nicht nur den des letzten Laufs hier.
546
+ void ladeVerlauf();
547
+ }
498
548
  window.scrollTo(0, 0);
499
549
  }
500
550
  document.querySelectorAll('[data-ansicht]').forEach(function (k) {
@@ -503,26 +553,6 @@
503
553
 
504
554
  /* ================= ZIELE ================= */
505
555
 
506
- /** Hinweiszeile, wenn ein inaktiver Harness noch Links oder eine eigene Registrierung an diesem
507
- Ziel hat (E38 R7): Säubern-Weg direkt in der Karte. Von linkTeil und alleTeil geteilt. */
508
- function ruheZeile(z, hatEigene) {
509
- var rz = e('div', 'ruhezeile');
510
- var rsvg = document.createElementNS('http://www.w3.org/2000/svg', 'svg');
511
- rsvg.setAttribute('viewBox', '0 0 16 16');
512
- rsvg.setAttribute('stroke-width', '1.6');
513
- rsvg.setAttribute('aria-hidden', 'true');
514
- rsvg.innerHTML = '<circle cx="8" cy="8" r="6.5"/><path d="M8 4.5v4M8 10.5v.5"/>';
515
- rz.appendChild(rsvg);
516
- rz.appendChild(e('p', null, harnessName(z.ziel.harness) + ' ist inaktiv. '
517
- + (hatEigene ? 'hmgr pflegt diese Links nicht mehr.' : 'Ein harness-eigener Eintrag verweist noch auf dieses Ziel.')));
518
- var rk = e('button', 'knopf leise klein', 'Säubern');
519
- rk.type = 'button';
520
- rk.setAttribute('aria-label', harnessName(z.ziel.harness) + ' säubern');
521
- rk.addEventListener('click', function () { frageSaeubern(z.ziel.harness); });
522
- rz.appendChild(rk);
523
- return rz;
524
- }
525
-
526
556
  /** Eine Zeile im Fremdkasten für einen fremden Eintrag x an einem Ziel z. Mit mitZeichen=true
527
557
  steht davor das kleine Harness-Zeichen seines Fundorts (E53 Pkt. 5, „Alle"-Ansicht). Gibt
528
558
  neben dem li auch zurück, ob eine Quelle erkannt wurde (für den Erklärtext). */
@@ -770,13 +800,6 @@
770
800
  var eigene = z.eintraege.filter(function (ei) { return ei.art !== 'fremd'; });
771
801
  var fremde = z.eintraege.filter(function (ei) { return ei.art === 'fremd'; });
772
802
 
773
- // Harness abgeschaltet, Konfiguration liegt noch (E38, GRILL.md Entwurfskritik 4): der Weg,
774
- // sie loszuwerden, steht hier oben in der Karte. Zählt auch ohne Links, wenn die harness-eigene
775
- // Registrierung noch da ist (E38 R7), etwa Hermes' external_dirs-Eintrag ohne Links darunter.
776
- if (!harnessAktiv(z.ziel.harness) && (eigene.length > 0 || z.istRegistriert)) {
777
- teil.appendChild(ruheZeile(z, eigene.length > 0));
778
- }
779
-
780
803
  var kz = konfigZeile(z);
781
804
  if (kz) teil.appendChild(kz);
782
805
 
@@ -1022,15 +1045,8 @@
1022
1045
  kopf.setAttribute('aria-expanded', offen ? 'true' : 'false');
1023
1046
  kopf.appendChild(beschriftet(e('h3', null, zielTitel(z)), z.ziel.harness));
1024
1047
  kopf.appendChild(e('span', 'mono', pfadKurz(z.verzeichnis)));
1025
- if (!harnessAktiv(z.ziel.harness)) kopf.appendChild(e('span', 'ruhend', 'Harness inaktiv'));
1026
- if (zielBraucht(z)) {
1027
- // Zeichen statt Text-Badge: trägt die Farbe selbst (Emoji-Darstellung), aria-label hält den
1028
- // Akkordeon-Knopf-Namen brauchbar (role="img", die Bedeutung steht nicht im Glyph selbst).
1029
- var warnspan = e('span', 'warnung', '⚠️');
1030
- warnspan.setAttribute('role', 'img');
1031
- warnspan.setAttribute('aria-label', 'braucht Aufmerksamkeit');
1032
- kopf.appendChild(warnspan);
1033
- }
1048
+ var ausloeser = zielAusloeser(z);
1049
+ if (ausloeser > 0) kopf.appendChild(warnzeichen(ausloeser));
1034
1050
  // Der Kopf zählt Skills, nicht Einträge (E71), über dieselbe Bündelung, aus der der Körper zeilt.
1035
1051
  // Bei einem Harness fällt die Zahl mit der Zahl eigener Einträge zusammen, die Regel bleibt eine.
1036
1052
  var zahl = buendleEigene([z]).reihenfolge.length;
@@ -1074,17 +1090,10 @@
1074
1090
 
1075
1091
  /** Körper des Chips „Alle" einer Verzeichnis-Karte (E53 Pkt. 2, 4, 5): eine gemeinsame Liste
1076
1092
  statt gestapelter Harness-Abschnitte. Eigene Links gebündelt (Schlüssel Name+Quelle), fremde
1077
- Einträge und die Inaktiv-Zeile je Harness mit dessen Zeichen am Fundort. */
1093
+ Einträge je Harness mit dessen Zeichen am Fundort. */
1078
1094
  function alleTeil(pfad, teile) {
1079
1095
  var teil = document.createDocumentFragment();
1080
1096
 
1081
- teile.forEach(function (z) {
1082
- var eigeneZ = z.eintraege.filter(function (ei) { return ei.art !== 'fremd'; });
1083
- if (!harnessAktiv(z.ziel.harness) && (eigeneZ.length > 0 || z.istRegistriert)) {
1084
- teil.appendChild(ruheZeile(z, eigeneZ.length > 0));
1085
- }
1086
- });
1087
-
1088
1097
  var beachteteAlle = [], ignorierteAlle = [];
1089
1098
  teile.forEach(function (z) {
1090
1099
  var ignoriertNamen = z.ignoriert || [];
@@ -1267,12 +1276,10 @@
1267
1276
  .forEach(function (z) { symbole.appendChild(harnessZeichen(z.ziel.harness)); });
1268
1277
  kopf.appendChild(symbole);
1269
1278
 
1270
- if (teile.some(zielBraucht)) {
1271
- var warnspan = e('span', 'warnung', '⚠️');
1272
- warnspan.setAttribute('role', 'img');
1273
- warnspan.setAttribute('aria-label', 'braucht Aufmerksamkeit');
1274
- kopf.appendChild(warnspan);
1275
- }
1279
+ // An der Verzeichnis-Karte ist die Warnzahl die Summe über die Teile (E75): der Nutzer sieht
1280
+ // hier einen Ordner, nicht die Harness-Abschnitte darunter.
1281
+ var ausloeserGesamt = teile.reduce(function (summe, z) { return summe + zielAusloeser(z); }, 0);
1282
+ if (ausloeserGesamt > 0) kopf.appendChild(warnzeichen(ausloeserGesamt));
1276
1283
  // Skills statt Einträge (E71): eine Summe über die Harness-Abschnitte zählte jeden Skill so oft,
1277
1284
  // wie ihn Harnesses am selben Verzeichnis führen — der Körper bündelt sie zu einer Zeile.
1278
1285
  var gesamtZahl = buendleEigene(teile).reihenfolge.length;
@@ -1323,7 +1330,8 @@
1323
1330
  var gewaehlt = teile.filter(function (z) { return z.ziel.harness === aktivChip; })[0];
1324
1331
  // Welcher Ordner im Verzeichnis gelesen wird, stand vorher im Abschnittskopf (25cddf6) und
1325
1332
  // gehört zum gewählten Harness, nicht zur Karte: er steht rechts in der Chip-Leiste, relativ
1326
- // zum Verzeichnis, mit der Inaktiv-Marke daneben.
1333
+ // zum Verzeichnis. Chips gibt es nur für aktive Harnesses (E74), eine Inaktiv-Marke braucht
1334
+ // die Leiste darum nicht mehr.
1327
1335
  if (gewaehlt) {
1328
1336
  // Nur die Anzeige: liegt der gelesene Ordner unterhalb des Verzeichnisses, steht er
1329
1337
  // relativ, sonst gekürzt. Beide Trenner, sonst greift das unter Windows nie (E68).
@@ -1331,7 +1339,6 @@
1331
1339
  || gewaehlt.verzeichnis.indexOf(pfad + '\\') === 0;
1332
1340
  chipleiste.appendChild(e('span', 'kartenordner mono',
1333
1341
  unterhalb ? gewaehlt.verzeichnis.slice(pfad.length + 1) : pfadKurz(gewaehlt.verzeichnis)));
1334
- if (!harnessAktiv(aktivChip)) chipleiste.appendChild(e('span', 'ruhend', 'Harness inaktiv'));
1335
1342
  }
1336
1343
  koerper.appendChild(chipleiste);
1337
1344
  if (gewaehlt) koerper.appendChild(linkTeil(gewaehlt));
@@ -1799,12 +1806,44 @@
1799
1806
  return d.toLocaleDateString('de-DE', { day: '2-digit', month: '2-digit', year: 'numeric' });
1800
1807
  }
1801
1808
 
1809
+ /** Reihenfolge der Skills-Indizes für die Katalog-Ansicht (E79, Richtung E81). SKILLS selbst
1810
+ bleibt unverändert (Name, dann Quelle sortiert, siehe ladeAlles), damit li.dataset.index
1811
+ stabil bleibt. Bei 'name'/'ab' einfach umgekehrt. Bei 'datum' stehen je nach Richtung die
1812
+ jüngsten oder die ältesten zuerst, Skills ohne Datum in BEIDEN Richtungen ganz hinten, bei
1813
+ Gleichstand (Datum oder „ohne Datum") in beiden Richtungen wie bisher nach Name/Quelle (also
1814
+ die ursprüngliche Reihenfolge). */
1815
+ function skillReihenfolge() {
1816
+ var indizes = SKILLS.map(function (_, i) { return i; });
1817
+ if (skillSortierung === 'name') {
1818
+ if (skillRichtung === 'ab') indizes.reverse();
1819
+ return indizes;
1820
+ }
1821
+ indizes.sort(function (a, b) {
1822
+ // Numerisch vergleichen, nicht als String: geaendert ist ISO-8601 mit dem Zeitzonen-Offset
1823
+ // des Committers (git %cI), zwei verschiedene Offsets ordnet ein String-Vergleich falsch.
1824
+ // Nicht parsebar zählt wie „ohne Datum" (hinten, in beiden Richtungen).
1825
+ var ta = SKILLS[a].geaendert ? Date.parse(SKILLS[a].geaendert) : NaN;
1826
+ var tb = SKILLS[b].geaendert ? Date.parse(SKILLS[b].geaendert) : NaN;
1827
+ var oa = isNaN(ta);
1828
+ var ob = isNaN(tb);
1829
+ if (oa && ob) return a - b;
1830
+ if (oa) return 1;
1831
+ if (ob) return -1;
1832
+ if (ta === tb) return a - b;
1833
+ if (skillRichtung === 'auf') return ta < tb ? -1 : 1;
1834
+ return ta < tb ? 1 : -1;
1835
+ });
1836
+ return indizes;
1837
+ }
1838
+
1802
1839
  function baueSkills() {
1803
1840
  var liste = $('katalog');
1804
1841
  liste.textContent = '';
1805
- SKILLS.forEach(function (skill, i) {
1842
+ skillReihenfolge().forEach(function (i) {
1843
+ var skill = SKILLS[i];
1806
1844
  var li = e('li', 'zeile');
1807
1845
  li.dataset.index = i;
1846
+ if (skill.geaendert) li.dataset.geaendert = skill.geaendert;
1808
1847
  var idx = quellenIndex(skill.quelleText);
1809
1848
  li.style.borderLeftColor = 'var(--q' + (idx + 1) + ')';
1810
1849
 
@@ -1903,6 +1942,32 @@
1903
1942
  }
1904
1943
  $('suchfeld').addEventListener('input', filtereSkills);
1905
1944
 
1945
+ /** Sortierknöpfe der Skills-Rubrik (E79, Richtung E81): statisches Markup, deshalb Listener nur
1946
+ einmal gebunden, nicht in baueSkills. Klick auf den bereits aktiven Chip kehrt nur die
1947
+ Richtung um; Klick auf den anderen Chip wechselt den Schlüssel und setzt dessen
1948
+ Standardrichtung. Nur der aktive Chip trägt den Pfeil (↑/↓) und data-richtung, das liest der
1949
+ Smoke. */
1950
+ function setzeSkillSortierung(wert) {
1951
+ if (wert === skillSortierung) {
1952
+ skillRichtung = skillRichtung === 'auf' ? 'ab' : 'auf';
1953
+ } else {
1954
+ skillSortierung = wert;
1955
+ skillRichtung = SKILL_RICHTUNG_STANDARD[wert];
1956
+ }
1957
+ var pfeil = skillRichtung === 'auf' ? '↑' : '↓';
1958
+ [['sortnach-name', 'name'], ['sortnach-datum', 'datum']].forEach(function (paar) {
1959
+ var chip = $(paar[0]);
1960
+ var aktiv = skillSortierung === paar[1];
1961
+ chip.setAttribute('aria-pressed', aktiv ? 'true' : 'false');
1962
+ chip.querySelector('.sortpfeil').textContent = aktiv ? pfeil : '';
1963
+ if (aktiv) chip.dataset.richtung = skillRichtung;
1964
+ else delete chip.dataset.richtung;
1965
+ });
1966
+ baueSkills();
1967
+ }
1968
+ $('sortnach-name').addEventListener('click', function () { setzeSkillSortierung('name'); });
1969
+ $('sortnach-datum').addEventListener('click', function () { setzeSkillSortierung('datum'); });
1970
+
1906
1971
  var laufendeSkill = null;
1907
1972
  function starteVerlinken(index) {
1908
1973
  laufendeSkill = index;
@@ -1910,7 +1975,14 @@
1910
1975
  $('dz-skill').textContent = skill.name;
1911
1976
  var box = $('dz-ziele');
1912
1977
  box.textContent = '';
1913
- ZIELE.forEach(function (z) {
1978
+ // E76: Ziele inaktiver Harnesses stehen gar nicht erst zur Wahl — der Kern lehnte den Link
1979
+ // ohnehin ab, und seit E74 zeigt keine Ansicht sie. Dieselbe Regel wie die Zielliste.
1980
+ var waehlbar = ZIELE.filter(zielSichtbar);
1981
+ if (waehlbar.length === 0) {
1982
+ box.appendChild(e('p', 'leerhinweis',
1983
+ 'Kein Ziel wählbar: alle Harnesses sind deaktiviert. Unter Einstellungen einen aktivieren.'));
1984
+ }
1985
+ waehlbar.forEach(function (z) {
1914
1986
  var schon = z.eintraege.some(function (ei) {
1915
1987
  var n = ei.art === 'link' ? ei.link.name : ei.name;
1916
1988
  return n === skill.name;
@@ -2319,100 +2391,140 @@
2319
2391
  /* ================= AKTUALISIEREN ================= */
2320
2392
 
2321
2393
  /**
2322
- * Ein Aktualisieren-Lauf, von Hand über den Knopf oder selbsttätig angestoßen (E44). Eine Sperre
2394
+ * Ein Aktualisieren-Lauf, von Hand über den Kringel oder selbsttätig angestoßen (Start, Intervall
2395
+ * — E44) — `ausloeser` sagt, welcher, und geht an den Kern für den Verlauf (E80). Eine Sperre
2323
2396
  * über den disabled-Zustand des Knopfs verhindert, dass ein Anstoß während eines laufenden Laufs
2324
- * etwas tut. Automatische Läufe rufen den Kern ohne Rückfrage, sie räumen also nichts weg.
2397
+ * etwas tut. Start und Intervall rufen den Kern ohne Rückfrage, sie räumen also nichts weg.
2325
2398
  */
2326
- /* Stand-Zeile der Seitenleiste (Entwurf): hervorgehoben „N Quellen mit Neuem" nach einem
2327
- automatischen Lauf mit Änderungen, bis die Update-Ansicht den Bericht zeigt; sonst ruhig
2328
- „Alles aktuell, HH:MM geprüft", oder bei Fehlschlägen (E51) „Prüfung fehlgeschlagen, HH:MM"
2329
- (alle Quellen-Abrufe gescheitert) bzw. „Geprüft mit Fehlern, HH:MM" (Mischung). Das „mit
2330
- Neuem"-Signal hat Vorrang: es kündigt sich sofort nach einem automatischen Lauf an, die
2331
- Fehlerkennzeichnung gilt erst wieder im ruhigen Zustand danach. */
2399
+ /* Stand-Zeile, gemeinsam für Seitenleiste (#standtext) und Update-Ansicht (#updatestand) (E80):
2400
+ vor dem ersten Lauf „Noch nicht geprüft", während eines Laufs „Prüft …", danach ruhig „Alles
2401
+ aktuell, HH:MM geprüft", oder bei Fehlschlägen (E51) „Prüfung fehlgeschlagen, HH:MM" (alle
2402
+ Quellen-Abrufe gescheitert) bzw. „Geprüft mit Fehlern, HH:MM" (Mischung). Hat ein Start- oder
2403
+ Intervall-Lauf Neues gebracht und die Ansicht war noch nicht offen, hervorgehoben „N Skills
2404
+ aktualisiert, HH:MM" (Fallback „N Quellen"/„N Bundles mit Neuem, HH:MM") — dieses Signal hat
2405
+ Vorrang, bis die Ansicht öffnet (zeigeAnsicht räumt `standNeu` dann ab) oder ein neuer Lauf es
2406
+ ersetzt. `.neu` am Standknopf bleibt die Hervorhebung, solange der Zustand gilt. */
2332
2407
  var standZeit = null;
2333
2408
  var standFehlerZustand = 'keine';
2334
- function standRuhig() {
2335
- var text = 'Alles aktuell, ' + standZeit + ' geprüft';
2336
- if (standZeit === null) text = 'Noch nicht aktualisiert';
2337
- else if (standFehlerZustand === 'alle') text = 'Prüfung fehlgeschlagen, ' + standZeit;
2338
- else if (standFehlerZustand === 'gemischt') text = 'Geprüft mit Fehlern, ' + standZeit;
2339
- $('standtext').textContent = text;
2340
- $('standknopf').classList.remove('neu');
2341
- $('standknopf').textContent = 'Prüfen';
2342
- }
2343
- function standNachLauf(ergebnis, automatisch, uhrzeit) {
2409
+ var standLaeuft = false;
2410
+ var standNeu = null; // { zahlwort: string, anhang: string } | null
2411
+
2412
+ function fuelleStand(el) {
2413
+ el.textContent = '';
2414
+ if (standLaeuft) { el.textContent = 'Prüft …'; return; }
2415
+ if (standNeu !== null) {
2416
+ el.appendChild(e('b', null, standNeu.zahlwort));
2417
+ el.appendChild(document.createTextNode(' ' + standNeu.anhang + ', ' + standZeit));
2418
+ return;
2419
+ }
2420
+ var text = 'Noch nicht geprüft';
2421
+ if (standZeit !== null) {
2422
+ text = standFehlerZustand === 'alle' ? 'Prüfung fehlgeschlagen, ' + standZeit
2423
+ : standFehlerZustand === 'gemischt' ? 'Geprüft mit Fehlern, ' + standZeit
2424
+ : 'Alles aktuell, ' + standZeit + ' geprüft';
2425
+ }
2426
+ el.textContent = text;
2427
+ }
2428
+ function standText() {
2429
+ fuelleStand($('standtext'));
2430
+ fuelleStand($('updatestand'));
2431
+ $('standknopf').classList.toggle('neu', standNeu !== null);
2432
+ }
2433
+
2434
+ function standNachLauf(ergebnis, ausloeser, uhrzeit) {
2344
2435
  standZeit = uhrzeit;
2345
2436
  var quellen = {};
2346
2437
  ergebnis.checkouts.forEach(function (c) {
2347
2438
  if (c.aktualisierung.ergebnis === 'aktualisiert') quellen[quelleKurz(c.checkout.quelle)] = true;
2348
2439
  });
2349
2440
  var neue = Object.keys(quellen).length;
2350
- // Skill-Ebene (E70): Namen über alle Checkouts, doppelte zählen einmal. Damit sagt der
2351
- // Standknopf „5 Skills aktualisiert" statt nur „1 Quelle mit Neuem".
2441
+ // Skill-Ebene (E70): Namen über alle Checkouts, doppelte zählen einmal. Damit sagt die
2442
+ // Stand-Zeile „5 Skills aktualisiert" statt nur „1 Quelle mit Neuem".
2352
2443
  var geaenderteSkills = {};
2353
2444
  ergebnis.checkouts.forEach(function (c) {
2354
2445
  (c.geaenderteSkills || []).forEach(function (name) { geaenderteSkills[name] = true; });
2355
2446
  });
2356
2447
  var skillZahl = Object.keys(geaenderteSkills).length;
2357
- // Ein nachgezogenes Bundle ist Neues wie ein neuer Commit (E69, F64=O1): sonst bliebe der
2358
- // Standknopf still, obwohl sich am Ziel gerade Links geändert haben.
2448
+ // Ein nachgezogenes Bundle ist Neues wie ein neuer Commit (E69, F64=O1): sonst bliebe die
2449
+ // Stand-Zeile still, obwohl sich am Ziel gerade Links geändert haben.
2359
2450
  var bundlesNeu = (ergebnis.bundles || []).filter(function (b) { return b.ergebnis === 'aktualisiert'; }).length;
2360
2451
  var fehlgeschlagen = ergebnis.checkouts.filter(function (c) { return c.aktualisierung.ergebnis === 'fehlgeschlagen'; }).length;
2361
2452
  var gesamt = ergebnis.checkouts.length;
2362
2453
  standFehlerZustand = fehlgeschlagen === 0 ? 'keine' : fehlgeschlagen === gesamt ? 'alle' : 'gemischt';
2363
- // Von Hand läuft es in der Update-Ansicht, der Bericht ist schon sichtbar: nichts anzukündigen.
2364
- if ((neue === 0 && bundlesNeu === 0) || !automatisch || !$('ansicht-update').hidden) return standRuhig();
2365
- var p = $('standtext');
2366
- p.textContent = '';
2367
- // Ist die Skill-Zahl bekannt, sagt sie mehr als die Zahl der Quellen (E70). Sonst bleibt es
2368
- // beim alten Signal: eine Quelle kann Neues haben, ohne dass ein Skill davon betroffen ist.
2369
- if (skillZahl > 0) {
2370
- p.appendChild(e('b', null, zahlwort(skillZahl, 'Skill', 'Skills')));
2371
- p.appendChild(document.createTextNode(' aktualisiert'));
2454
+ // Von Hand (Klick) läuft es in der Update-Ansicht, die schon offen ist: nichts anzukündigen.
2455
+ var automatisch = ausloeser !== 'klick';
2456
+ if ((neue === 0 && bundlesNeu === 0) || !automatisch || !$('ansicht-update').hidden) {
2457
+ standNeu = null;
2458
+ } else if (skillZahl > 0) {
2459
+ // Ist die Skill-Zahl bekannt, sagt sie mehr als die Zahl der Quellen (E70). Sonst bleibt es
2460
+ // beim alten Signal: eine Quelle kann Neues haben, ohne dass ein Skill davon betroffen ist.
2461
+ standNeu = { zahlwort: zahlwort(skillZahl, 'Skill', 'Skills'), anhang: 'aktualisiert' };
2372
2462
  } else {
2373
- p.appendChild(e('b', null, neue > 0
2374
- ? zahlwort(neue, 'Quelle', 'Quellen')
2375
- : zahlwort(bundlesNeu, 'Bundle', 'Bundles')));
2376
- p.appendChild(document.createTextNode(' mit Neuem'));
2463
+ standNeu = {
2464
+ zahlwort: neue > 0 ? zahlwort(neue, 'Quelle', 'Quellen') : zahlwort(bundlesNeu, 'Bundle', 'Bundles'),
2465
+ anhang: 'mit Neuem',
2466
+ };
2377
2467
  }
2378
- $('standknopf').classList.add('neu');
2379
- $('standknopf').textContent = 'Ansehen';
2468
+ standText();
2380
2469
  }
2381
2470
 
2382
- async function aktualisierenLauf(automatisch) {
2471
+ async function aktualisierenLauf(ausloeser) {
2383
2472
  var knopf = $('updatestart');
2384
2473
  if (knopf.disabled) return;
2385
2474
  knopf.disabled = true;
2386
- knopf.textContent = 'Läuft …';
2387
- $('standtext').textContent = 'Prüft …';
2475
+ knopf.classList.add('laeuft');
2476
+ standLaeuft = true;
2477
+ standText();
2478
+ var lauf = rufe(function () {
2479
+ return window.hmgr.aktualisieren({ rueckfragen: ausloeser !== 'klick', ausloeser: ausloeser });
2480
+ });
2481
+ // Jeder Lauf prüft auch hmgr selbst (E80): angestoßen, nicht abgewartet — ein Fund meldet sich
2482
+ // später von sich aus in der Seitenleiste (onSelbstUpdate).
2483
+ window.hmgr.selbstUpdatePruefen();
2388
2484
  try {
2389
- var ergebnis = await rufe(function () {
2390
- return automatisch ? window.hmgr.aktualisieren({ rueckfragen: false }) : window.hmgr.aktualisieren();
2391
- });
2392
- baueBericht(ergebnis);
2485
+ var ergebnis = await lauf;
2393
2486
  var uhrzeit = new Date().toLocaleTimeString('de-DE', { hour: '2-digit', minute: '2-digit' });
2394
- var ausloeser = automatisch ? 'automatisch' : 'von Hand';
2395
- $('updatestand').textContent = 'Zuletzt ' + uhrzeit + ', ' + ausloeser + ', ' + ergebnis.checkouts.length + ' Checkouts.';
2396
- standNachLauf(ergebnis, automatisch, uhrzeit);
2487
+ standNachLauf(ergebnis, ausloeser, uhrzeit);
2488
+ await ladeVerlauf();
2397
2489
  await ladeAlles();
2398
2490
  } finally {
2491
+ standLaeuft = false;
2399
2492
  knopf.disabled = false;
2400
- knopf.textContent = 'Jetzt aktualisieren';
2493
+ knopf.classList.remove('laeuft');
2401
2494
  // Scheiterte der Lauf, bliebe sonst „Prüft …" stehen.
2402
- if ($('standtext').textContent === 'Prüft …') standRuhig();
2403
- }
2404
- }
2405
-
2406
- $('updatestart').addEventListener('click', function () { aktualisierenLauf(false); });
2407
- // Der Standknopf sagt, was ein Klick tut: ruhig „Prüfen" (Ansicht öffnen und Lauf starten),
2408
- // mit Neuem „Ansehen" (nur den Bericht zeigen, der Lauf ist schon gelaufen). Die Klasse muss
2409
- // vor zeigeAnsicht gelesen werden, denn die räumt den neu-Zustand ab.
2410
- $('standknopf').addEventListener('click', function () {
2411
- var neu = this.classList.contains('neu');
2412
- zeigeAnsicht('update');
2413
- if (!neu) aktualisierenLauf(false);
2414
- });
2415
- window.hmgr.onAktualisierenAnstossen(function () { aktualisierenLauf(true); });
2495
+ if ($('standtext').textContent === 'Prüft …') standText();
2496
+ }
2497
+ }
2498
+
2499
+ /* Update-Hinweise über hmgr selbst (E77/E80), unten in der Seitenleiste über dem
2500
+ Einstellungen-Knopf. Zwei getrennte Zeilen, weil beide zugleich zutreffen können: ein
2501
+ geladenes GUI-Paket und eine neuere CLI auf npm sind zwei Pakete und zwei Kanäle (Gitea bzw.
2502
+ npm). Nur die GUI-Zeile trägt seit E80 einen Knopf (Neustart installiert das geladene Paket),
2503
+ die CLI installiert die GUI nicht selbst (F82). */
2504
+ function selbstUpdateZeigen(nachricht) {
2505
+ if (nachricht.art === 'gui') {
2506
+ $('selbstupdate-gui').textContent = 'hmgr ' + nachricht.version + ' bereit — wird beim Beenden installiert';
2507
+ $('selbstupdate-gui').hidden = false;
2508
+ $('selbstupdate-neustart').hidden = false;
2509
+ } else if (nachricht.art === 'cli') {
2510
+ var p = $('selbstupdate-cli');
2511
+ p.textContent = '';
2512
+ p.appendChild(document.createTextNode('hmgr-CLI ' + nachricht.installiert + ' — npm hat ' + nachricht.neu + ': '));
2513
+ p.appendChild(e('code', null, nachricht.befehl));
2514
+ p.hidden = false;
2515
+ } else return;
2516
+ $('selbstupdate').hidden = false;
2517
+ // Die Datei hat gerade ein hmgr-Ereignis bekommen (E80): der Verlauf zeigt den aktuellen Stand.
2518
+ void ladeVerlauf();
2519
+ }
2520
+ window.hmgr.onSelbstUpdate(selbstUpdateZeigen);
2521
+ $('selbstupdate-neustart').addEventListener('click', function () { window.hmgr.neuStarten(); });
2522
+
2523
+ $('updatestart').addEventListener('click', function () { aktualisierenLauf('klick'); });
2524
+ // Der Standknopf öffnet nur noch die Ansicht (E80): kein eigener Lauf mehr an diesem Klick, der
2525
+ // Kringel in der Ansicht startet ihn bei Bedarf.
2526
+ $('standknopf').addEventListener('click', function () { zeigeAnsicht('update'); });
2527
+ window.hmgr.onAktualisierenAnstossen(function (ausloeser) { aktualisierenLauf(ausloeser); });
2416
2528
 
2417
2529
  // Stilles Neuladen bei Änderungen außerhalb der GUI (E62, Fokus-Wechsel und Datei-Watcher): nur
2418
2530
  // ladeAlles, keine Meldung, kein Spinner-Zwang. Nachrutsch-sicher: höchstens ein Durchlauf läuft,
@@ -2443,89 +2555,76 @@
2443
2555
  }
2444
2556
  window.hmgr.onNeuLadenAnstossen(function () { void stillerReload(); });
2445
2557
 
2446
- function baueBericht(ergebnis) {
2447
- var box = $('bericht');
2448
- box.textContent = '';
2449
- box.hidden = false;
2450
-
2451
- var checkoutBlock = e('div', 'block');
2452
- checkoutBlock.appendChild(e('span', 'etikett', 'Checkouts'));
2453
- var cul = e('ul');
2454
- if (ergebnis.checkouts.length === 0) cul.appendChild(e('li', null, 'Keine Checkouts.'));
2455
- ergebnis.checkouts.forEach(function (c) {
2456
- var li = e('li');
2457
- var a = c.aktualisierung;
2458
- var art = a.ergebnis === 'aktualisiert' || a.ergebnis === 'unveraendert' ? 'ok' : 'aus';
2459
- var text = a.ergebnis === 'aktualisiert' ? 'aktualisiert'
2460
- : a.ergebnis === 'unveraendert' ? 'unverändert'
2461
- : a.ergebnis === 'fehlgeschlagen' ? 'fehlgeschlagen (' + a.fehler + ')'
2462
- : 'übersprungen (lokale Änderung)';
2463
- li.appendChild(e('span', 'marke-' + art, text));
2464
- // Skill-Ebene an der Checkout-Zeile (E70): sonst steht hier nur, dass die Quelle neue
2465
- // Commits hat, und wer sein eigenes Push sucht, findet es nicht wieder.
2466
- var skills = c.geaenderteSkills || [];
2467
- li.appendChild(quelleSpan('pfad', c.checkout.quelle, ' @ ' + c.checkout.ref
2468
- + (skills.length > 0 ? ' · ' + zahlwort(skills.length, 'Skill', 'Skills') + ': ' + skills.join(', ') : '')));
2469
- cul.appendChild(li);
2470
- });
2471
- checkoutBlock.appendChild(cul);
2472
- box.appendChild(checkoutBlock);
2473
-
2474
- if (ergebnis.pluginHarnesses.length > 0) {
2475
- var pluginBlock = e('div', 'block');
2476
- pluginBlock.appendChild(e('span', 'etikett', 'Plugins'));
2477
- var pul = e('ul');
2478
- ergebnis.pluginHarnesses.forEach(function (name) {
2479
- var li = e('li');
2480
- li.appendChild(e('span', 'marke-ok', 'aktualisiert'));
2481
- li.appendChild(e('span', 'pfad', name));
2482
- pul.appendChild(li);
2483
- });
2484
- pluginBlock.appendChild(pul);
2485
- box.appendChild(pluginBlock);
2486
- }
2487
-
2488
- // Bundles im selben Lauf (E69, F64=O1): eine geänderte Definition wird beim Aktualisieren
2489
- // nachgezogen, eine Zeile je Bundle im Stil der Checkout-Zeilen darüber.
2490
- var bundleEintraege = ergebnis.bundles || [];
2491
- if (bundleEintraege.length > 0) {
2492
- var bundleBlock = e('div', 'block');
2493
- bundleBlock.appendChild(e('span', 'etikett', 'Bundles'));
2494
- var bul = e('ul');
2495
- bundleEintraege.forEach(function (b) {
2496
- var li = e('li');
2497
- var text = b.ergebnis === 'aktualisiert' ? 'aktualisiert'
2498
- : b.ergebnis === 'unveraendert' ? 'unverändert'
2499
- : 'fehlgeschlagen (' + (b.fehler || '').split('\n')[0] + ')';
2500
- li.appendChild(e('span', b.ergebnis === 'fehlgeschlagen' ? 'marke-aus' : 'marke-ok', text));
2501
- // Was sich geändert hat, steht hinter dem Namen; ein Fehlschlag sagt, dass nichts geschah.
2502
- var teile = [];
2503
- if (b.gelegt.length > 0) teile.push('+ ' + b.gelegt.join(', '));
2504
- if (b.entfernt.length > 0) teile.push('− ' + b.entfernt.join(', '));
2505
- if (b.neueQuellen.length > 0) teile.push('neue Quellen: ' + b.neueQuellen.length);
2506
- if (b.ergebnis === 'fehlgeschlagen') teile.push('bleibt auf dem alten Stand');
2507
- li.appendChild(e('span', 'pfad', b.bundle + ' · ' + harnessName(b.ziel.harness) + ' · ' + ortName(b.ziel.ort)
2508
- + (teile.length > 0 ? ' · ' + teile.join(' · ') : '')));
2509
- bul.appendChild(li);
2510
- });
2511
- bundleBlock.appendChild(bul);
2512
- box.appendChild(bundleBlock);
2558
+ /**
2559
+ * Eine Ereigniszeile im Verlauf (E80): Marke + Text, Muster wie die frühere baueBericht-Zeile
2560
+ * (Checkout/Bundle) und die CLI-Ausgabe (verlaufEreignisZeile, src/cli/main.ts).
2561
+ */
2562
+ function verlaufEreignisZeile(li, ereignis) {
2563
+ if (ereignis.art === 'checkout') {
2564
+ li.appendChild(e('span', 'marke-ok', 'aktualisiert'));
2565
+ var skills = ereignis.skills.length > 0
2566
+ ? ' · ' + zahlwort(ereignis.skills.length, 'Skill', 'Skills') + ': ' + ereignis.skills.join(', ')
2567
+ : '';
2568
+ var stand = ' (' + ereignis.von.slice(0, 7) + ' → ' + ereignis.nach.slice(0, 7) + ')';
2569
+ li.appendChild(e('span', 'pfad', ereignis.quelle + ' @ ' + ereignis.ref + skills + stand));
2570
+ } else if (ereignis.art === 'bundle') {
2571
+ li.appendChild(e('span', 'marke-ok', 'Bundle'));
2572
+ var teile = [];
2573
+ if (ereignis.gelegt.length > 0) teile.push('gelegt ' + ereignis.gelegt.join(', '));
2574
+ if (ereignis.entfernt.length > 0) teile.push('entfernt ' + ereignis.entfernt.join(', '));
2575
+ // Ziel als Objekt seit E80-Nachtrag; ein String-Ziel (ältere Einträge) bleibt unverändert.
2576
+ var zielText = typeof ereignis.ziel === 'string'
2577
+ ? ereignis.ziel
2578
+ : harnessName(ereignis.ziel.harness) + ' · ' + ortName(ereignis.ziel.ort);
2579
+ li.appendChild(e('span', 'pfad', ereignis.bundle + ' → ' + zielText
2580
+ + (teile.length > 0 ? ' · ' + teile.join(' · ') : '')));
2581
+ } else if (ereignis.art === 'aufgeraeumt') {
2582
+ li.appendChild(e('span', 'marke-ok', 'aufgeräumt'));
2583
+ li.appendChild(e('span', 'pfad', ereignis.checkout));
2584
+ } else if (ereignis.art === 'fehler') {
2585
+ li.appendChild(e('span', 'marke-aus', 'fehlgeschlagen'));
2586
+ li.appendChild(e('span', 'pfad', ereignis.betrifft + ': ' + ereignis.fehler));
2587
+ } else if (ereignis.art === 'hmgr-installiert') {
2588
+ li.appendChild(e('span', 'marke-ok', 'installiert'));
2589
+ li.appendChild(e('span', 'pfad', (ereignis.paket === 'gui' ? 'hmgr-GUI ' : 'hmgr ') + ereignis.version));
2590
+ } else if (ereignis.art === 'gui-update-geladen') {
2591
+ li.appendChild(e('span', 'marke-ok', 'geladen'));
2592
+ li.appendChild(e('span', 'pfad', 'GUI-Update ' + ereignis.version));
2593
+ } else if (ereignis.art === 'cli-verfuegbar') {
2594
+ li.appendChild(e('span', 'marke-ok', 'verfügbar'));
2595
+ li.appendChild(e('span', 'pfad', 'CLI ' + ereignis.version));
2513
2596
  }
2597
+ }
2514
2598
 
2515
- if (ergebnis.aufraeumen.verwaiste.length > 0) {
2516
- var raeumBlock = e('div', 'block entfernt');
2517
- raeumBlock.appendChild(e('span', 'etikett', 'Verwaiste Checkouts'));
2518
- var rul = e('ul');
2519
- ergebnis.aufraeumen.verwaiste.forEach(function (v) {
2599
+ /**
2600
+ * Baut das Changelog in der Update-Ansicht aus ~/.hmgr/verlauf.json (E80): ein Block je Lauf,
2601
+ * neueste zuerst, das Etikett der lokale Zeitpunkt, darunter eine Zeile je Ereignis. Die Datei
2602
+ * ist die Wahrheit, kein eigener Zustand im Renderer — jeder Aufruf holt sie neu.
2603
+ */
2604
+ async function ladeVerlauf() {
2605
+ var eintraege = await rufe(function () { return window.hmgr.verlauf(); });
2606
+ var box = $('verlauf');
2607
+ box.textContent = '';
2608
+ if (eintraege.length === 0) {
2609
+ box.appendChild(e('p', 'leerhinweis', 'Noch keine Ereignisse.'));
2610
+ return;
2611
+ }
2612
+ eintraege.forEach(function (eintrag) {
2613
+ var block = e('div', 'block');
2614
+ var zeit = new Date(eintrag.zeit);
2615
+ var zwei = function (n) { return String(n).padStart(2, '0'); };
2616
+ var etikett = zwei(zeit.getDate()) + '.' + zwei(zeit.getMonth() + 1) + '.' + zeit.getFullYear()
2617
+ + ' ' + zwei(zeit.getHours()) + ':' + zwei(zeit.getMinutes());
2618
+ block.appendChild(e('span', 'etikett', etikett));
2619
+ var ul = e('ul');
2620
+ eintrag.ereignisse.forEach(function (ereignis) {
2520
2621
  var li = e('li');
2521
- var text = v.geloescht ? 'gelöscht' : v.lokaleAenderung ? 'liegen gelassen (lokale Änderung)' : 'liegen gelassen';
2522
- li.appendChild(e('span', v.geloescht ? 'marke-aus' : 'marke-aus', text));
2523
- li.appendChild(quelleSpan('pfad', v.checkout.quelle, ' @ ' + v.checkout.ref));
2524
- rul.appendChild(li);
2622
+ verlaufEreignisZeile(li, ereignis);
2623
+ ul.appendChild(li);
2525
2624
  });
2526
- raeumBlock.appendChild(rul);
2527
- box.appendChild(raeumBlock);
2528
- }
2625
+ block.appendChild(ul);
2626
+ box.appendChild(block);
2627
+ });
2529
2628
  }
2530
2629
 
2531
2630
  /* ================= PLUGINS ================= */
@@ -2999,6 +3098,132 @@
2999
3098
  + '. Nicht von hmgr verwaltete Einträge liegen unverändert in den Verzeichnissen.';
3000
3099
  }
3001
3100
 
3101
+ /* ---------------- Homedirs eines Harness (E78) ---------------- */
3102
+
3103
+ /**
3104
+ * Die Homedir-Zeilen unter einem Harness-Schalter (E78): das primäre mit „Ändern"/„Zurücksetzen"
3105
+ * (F89), darunter jedes zusätzliche mit „Vergessen" (F87), zuletzt der Knopf zum Eintragen eines
3106
+ * weiteren. Der Knopf steht nur bei einem Harness, der die Fähigkeit hat, und nur unter Windows —
3107
+ * dieselbe Grenze wie im Kern, hier nur damit niemand einen Weg vor sich sieht, der abbricht.
3108
+ */
3109
+ function heimeFuerHarness(h) {
3110
+ var kasten = e('div', 'heime');
3111
+ var eintrag = null;
3112
+ HARNESS_HEIME.forEach(function (x) { if (x.harness === h) eintrag = x; });
3113
+ if (!eintrag) return kasten;
3114
+ eintrag.heime.forEach(function (heim) {
3115
+ var zeile = e('div', 'heimzeile');
3116
+ zeile.appendChild(e('span', 'heimart', heim.adresse ? heim.adresse : 'Homedir'));
3117
+ var pfad = e('code', 'heimpfad', pfadKurz(heim.pfad));
3118
+ pfad.title = heim.pfad;
3119
+ zeile.appendChild(pfad);
3120
+ var knoepfe = e('span', 'heimknoepfe');
3121
+ if (heim.adresse) {
3122
+ var weg = e('button', 'knopf leise klein', 'Vergessen');
3123
+ weg.type = 'button';
3124
+ weg.setAttribute('aria-label', heim.adresse + ' bei ' + harnessName(h) + ' vergessen');
3125
+ weg.addEventListener('click', function () { frageHeimVergessen(h, heim.adresse); });
3126
+ knoepfe.appendChild(weg);
3127
+ } else {
3128
+ var aendern = e('button', 'knopf leise klein', 'Ändern …');
3129
+ aendern.type = 'button';
3130
+ aendern.setAttribute('aria-label', 'Homedir von ' + harnessName(h) + ' ändern');
3131
+ aendern.addEventListener('click', function () { oeffneHeimSetzen(h, heim.pfad); });
3132
+ knoepfe.appendChild(aendern);
3133
+ if (heim.eingestellt) {
3134
+ var zurueck = e('button', 'knopf leise klein', 'Zurücksetzen');
3135
+ zurueck.type = 'button';
3136
+ zurueck.addEventListener('click', function () {
3137
+ rufe(function () { return window.hmgr.harnessHeimZuruecksetzen(h); }).then(async function () {
3138
+ await ladeAlles();
3139
+ melde(harnessName(h) + ' liest sein Homedir wieder am Standardort. Am alten Ort bleibt alles liegen.');
3140
+ });
3141
+ });
3142
+ knoepfe.appendChild(zurueck);
3143
+ }
3144
+ }
3145
+ zeile.appendChild(knoepfe);
3146
+ kasten.appendChild(zeile);
3147
+ });
3148
+ if (eintrag.kannZusaetzliche && PLATTFORM === 'win32') {
3149
+ var neu = e('button', 'knopf leise klein', 'Homedir in WSL hinzufügen …');
3150
+ neu.type = 'button';
3151
+ neu.addEventListener('click', function () { oeffneHeimHinzufuegen(h); });
3152
+ kasten.appendChild(neu);
3153
+ }
3154
+ return kasten;
3155
+ }
3156
+
3157
+ var heimHarness = null;
3158
+ function oeffneHeimSetzen(h, jetzt) {
3159
+ heimHarness = h;
3160
+ $('dhh-titel').textContent = 'Homedir von ' + harnessName(h);
3161
+ $('dhh-pfad').value = jetzt;
3162
+ zeige($('d-heim-pfad'));
3163
+ $('dhh-pfad').focus();
3164
+ }
3165
+ nachDialog($('d-heim-pfad'), async function (wert) {
3166
+ var h = heimHarness;
3167
+ heimHarness = null;
3168
+ if (wert !== 'ok' || !h) return;
3169
+ var pfad = $('dhh-pfad').value.trim();
3170
+ if (!pfad) return;
3171
+ var gesetzt = await rufe(function () { return window.hmgr.harnessHeimSetzen({ harness: h, pfad: pfad }); });
3172
+ await ladeAlles();
3173
+ melde(harnessName(h) + ' liest sein Homedir jetzt unter ' + gesetzt
3174
+ + '. Was am alten Ort liegt, bleibt dort liegen — hmgr zeigt es nicht mehr.');
3175
+ });
3176
+
3177
+ async function oeffneHeimHinzufuegen(h) {
3178
+ heimHarness = h;
3179
+ var auswahl = $('dhw-distro');
3180
+ auswahl.textContent = '';
3181
+ var distros = await rufe(function () { return window.hmgr.wslDistros(); });
3182
+ (distros || []).forEach(function (name) {
3183
+ var option = document.createElement('option');
3184
+ option.value = name;
3185
+ option.textContent = name;
3186
+ auswahl.appendChild(option);
3187
+ });
3188
+ $('dhw-leer').hidden = (distros || []).length > 0;
3189
+ $('dhw-distro').hidden = (distros || []).length === 0;
3190
+ $('dhw-ok').disabled = (distros || []).length === 0;
3191
+ zeige($('d-heim-wsl'));
3192
+ }
3193
+ nachDialog($('d-heim-wsl'), async function (wert) {
3194
+ var h = heimHarness;
3195
+ heimHarness = null;
3196
+ if (wert !== 'ok' || !h) return;
3197
+ var distro = $('dhw-distro').value;
3198
+ if (!distro) return;
3199
+ var heim = await rufe(function () {
3200
+ return window.hmgr.harnessHeimHinzufuegen({ harness: h, adresse: 'wsl:' + distro });
3201
+ });
3202
+ await ladeAlles();
3203
+ melde(harnessName(h) + ' hat jetzt ein zweites Ziel: ' + heim.adresse + ' mit dem Homedir ' + heim.pfad
3204
+ + '. Links legst du dort einzeln, gespiegelt wird nichts.');
3205
+ });
3206
+
3207
+ function frageHeimVergessen(h, adresse) {
3208
+ heimHarness = h;
3209
+ $('dhv-titel').textContent = adresse + ' vergessen?';
3210
+ $('dhv-text').textContent = 'hmgr verwaltet dieses Homedir dann nicht mehr und zeigt sein Ziel nicht mehr an. '
3211
+ + 'Die Links, die dort liegen, bleiben liegen.';
3212
+ $('d-heim-weg').dataset.adresse = adresse;
3213
+ zeige($('d-heim-weg'));
3214
+ }
3215
+ nachDialog($('d-heim-weg'), async function (wert) {
3216
+ var h = heimHarness;
3217
+ heimHarness = null;
3218
+ var adresse = $('d-heim-weg').dataset.adresse;
3219
+ if (wert !== 'ok' || !h || !adresse) return;
3220
+ await rufe(function () { return window.hmgr.harnessHeimEntfernen({ harness: h, adresse: adresse }); });
3221
+ await ladeAlles();
3222
+ melde(harnessName(h) + ' verwaltet ' + adresse + ' nicht mehr. Die Links dort bleiben liegen.');
3223
+ });
3224
+
3225
+ enterBestaetigt($('dhh-pfad'), $('dhh-ok'));
3226
+
3002
3227
  function baueHarnessliste() {
3003
3228
  var ul = $('harnessliste');
3004
3229
  // Der Neuaufbau wirft den Schalter weg, auf dem der Fokus steht. Nach einer Rückfrage ist genau
@@ -3029,6 +3254,7 @@
3029
3254
  txt.appendChild(e('small', null, stand));
3030
3255
  label.appendChild(txt);
3031
3256
  li.appendChild(label);
3257
+ li.appendChild(heimeFuerHarness(h));
3032
3258
  ul.appendChild(li);
3033
3259
  });
3034
3260
  if (merk) {
@@ -3077,40 +3303,27 @@
3077
3303
  var liegen = bilanzText(harnessBilanz(h));
3078
3304
  await rufe(function () { return window.hmgr.harnessDeaktivieren(h); });
3079
3305
  await ladeAlles();
3080
- 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.');
3306
+ // E74: die Ziele verschwinden mit dem Harness aus der Ansicht, ein Säubern-Knopf im Ziel
3307
+ // existiert nicht mehr. Was bleibt, ist der Weg über das Wiedereinschalten.
3308
+ melde(harnessName(h) + ' deaktiviert. ' + liegen + ' bleiben liegen, hmgr pflegt sie nicht mehr. Die Ziele verschwinden aus der Liste und erscheinen wieder, sobald du ' + harnessName(h) + ' wieder einschaltest.');
3081
3309
  } else {
3082
3310
  // Abbruch: der Schalter im DOM steht noch auf aus, die Einstellung nicht. Neu aufbauen setzt ihn zurück.
3083
3311
  baueHarnessliste();
3084
3312
  }
3085
3313
  });
3086
3314
 
3087
- /* Derselbe Weg für Konfiguration, die schon ohne aktiven Harness dalag (Säubern-Knopf im Ziel). */
3088
- var saeuberKandidat = null;
3089
- function frageSaeubern(h) {
3090
- saeuberKandidat = h;
3091
- var bilanz = harnessBilanz(h);
3092
- $('ds-titel').textContent = harnessName(h) + ' säubern?';
3093
- beschriftet($('ds-titel'), h);
3094
- $('ds-text').textContent = harnessName(h) + ' ist inaktiv, Konfiguration liegt noch (' + bilanzText(bilanz)
3095
- + '). Säubern entfernt die hmgr-Links und harness-eigene Einträge, nicht von hmgr verwaltete Einträge bleiben.';
3096
- fuelleBilanzListe($('ds-liste'), bilanz);
3097
- zeige($('d-saeubern'));
3098
- }
3099
- nachDialog($('d-saeubern'), async function (wert) {
3100
- var h = saeuberKandidat;
3101
- saeuberKandidat = null;
3102
- if (wert !== 'los' || !h) return;
3103
- var ergebnis = await rufe(function () { return window.hmgr.saeubern(h); });
3104
- await ladeAlles();
3105
- melde(saeuberMeldung(harnessName(h) + ' gesäubert.', ergebnis));
3106
- });
3315
+ /* Einen eigenen Säubern-Weg für einen schon inaktiven Harness gibt es nicht mehr (E74): sein Ziel
3316
+ steht nicht mehr im Bild, also auch kein Knopf darin. Wer doch säubern will, schaltet den
3317
+ Harness ein und wieder aus — dann fragt der Dialog oben erneut. */
3107
3318
 
3108
3319
  function baueEinstellungen() {
3109
3320
  baueHarnessliste();
3110
3321
  var liste = $('quellenliste');
3111
3322
  liste.textContent = '';
3112
3323
  if (QUELLEN.length === 0) {
3113
- liste.appendChild(e('li', null, 'Noch keine Quelle. „Quelle hinzufügen …" legt die erste an.'));
3324
+ // Eigene Klasse statt nacktem Text im Listen-Grid (E75): `.quellenliste li` ist ein Grid mit
3325
+ // 20px-Icon-Spalte, in die der anonyme Textknoten fiel — jedes Wort brach einzeln um.
3326
+ liste.appendChild(e('li', 'leerhinweis', 'Noch keine Quelle. „Quelle hinzufügen …" legt die erste an.'));
3114
3327
  return;
3115
3328
  }
3116
3329
  QUELLEN.forEach(function (q) {