hmgr 0.0.6 → 0.0.7

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.
@@ -303,6 +303,11 @@
303
303
  var ZIELE = [];
304
304
  var SKILLS = [];
305
305
  var MARKETPLACES = [];
306
+ // Bundles aller bekannten Quellen (E69), entdeckt beim Quellen-Scan: kein eigener Katalog, keine
307
+ // eigene Ansicht. Grundlage der Bundle-Sektion im Dialog „Skills hinzufügen" (F68).
308
+ var BUNDLES = [];
309
+ // Definitionen, die sich nicht lesen ließen: genannt, damit eine kaputte Datei nicht stumm fehlt.
310
+ var BUNDLES_KAPUTT = [];
306
311
  var QUELLEN = [];
307
312
  var HARNESSE_ALLE = [];
308
313
  var AKTIVE_HARNESSE = [];
@@ -404,6 +409,7 @@
404
409
  rufe(function () { return window.hmgr.nichtVorhandeneHarnesses(); }),
405
410
  rufe(function () { return window.hmgr.mcpUebersicht(); }),
406
411
  rufe(function () { return window.hmgr.mcpPlaetze(); }),
412
+ rufe(function () { return window.hmgr.bundles(); }),
407
413
  ]);
408
414
  ZIELE = ergebnisse[0];
409
415
  SKILLS = ergebnisse[1];
@@ -418,6 +424,9 @@
418
424
  MCP_FREMD = ergebnisse[7].unbekannt;
419
425
  MCP_FREMD_GETEILT = ergebnisse[7].unbekannteGeteilteFundorte;
420
426
  MCP_PLAETZE = ergebnisse[8];
427
+ BUNDLES = ergebnisse[9].bundles;
428
+ BUNDLES.sort(function (a, b) { return a.name.localeCompare(b.name, 'de') || a.quelleText.localeCompare(b.quelleText, 'de'); });
429
+ BUNDLES_KAPUTT = ergebnisse[9].kaputte;
421
430
  STANDARD_REF = {};
422
431
  SKILLS.forEach(function (s) { if (STANDARD_REF[s.quelleText] === undefined) STANDARD_REF[s.quelleText] = s.ref; });
423
432
 
@@ -652,6 +661,110 @@
652
661
  return kz;
653
662
  }
654
663
 
664
+ /** Defekte Links (ohne Quelle) zuerst, sie brauchen Aufmerksamkeit (E45), sonst alphabetisch. */
665
+ function sortiereEintraege(eintraege) {
666
+ return eintraege.slice().sort(function (a, b) {
667
+ var da = a.art === 'link-ohne-quelle', db = b.art === 'link-ohne-quelle';
668
+ if (da !== db) return da ? -1 : 1;
669
+ var na = a.art === 'link' ? a.link.name : a.name;
670
+ var nb = b.art === 'link' ? b.link.name : b.name;
671
+ return na.localeCompare(nb, 'de');
672
+ });
673
+ }
674
+
675
+ /** Eine Zeile der Link-Tabelle. `bundle` gesetzt heißt: die Zeile steht in einer Bundle-Gruppe
676
+ (E69), das Entfernen zerbricht dort das Bundle und der Dialog sagt es. */
677
+ function linkZeile(ei, z, bundle) {
678
+ var name = ei.art === 'link' ? ei.link.name : ei.name;
679
+ var li = e('li');
680
+
681
+ if (ei.art === 'link') li.appendChild(repoIcon(ei.link.checkout.quelle));
682
+ else li.appendChild(repoIcon({ art: 'lokal', pfad: ei.checkoutPfad }));
683
+
684
+ li.appendChild(e('span', 'name', name));
685
+
686
+ // Die Beschreibung aus der SKILL.md (E39): die Frage, wegen der die Zeile gelesen wird,
687
+ // deshalb die breiteste Spalte. Ein Link ohne bekannte Quelle kennt seinen Skill nicht.
688
+ li.appendChild(wasZelle(ei.art === 'link' ? ei.link.skill : null));
689
+
690
+ // Quelle: der Name, anklickbar, dahinter nur, was vom Normalfall abweicht — ein Ref, das
691
+ // vom Standard-Ref der Quelle abweicht, und der Grund „über Zeiger" (E57). „Manuell" (der
692
+ // Normalfall) entfällt ersatzlos, ein Plugin-Grund steht stattdessen in der Aktionsspalte.
693
+ var qz = e('span', 'quelle');
694
+ if (ei.art === 'link') {
695
+ var einschraenkung = z.einschraenkungen[name];
696
+ if (einschraenkung) qz.appendChild(document.createTextNode(einschraenkung + ' → '));
697
+ qz.appendChild(quelleLinkElement(ei.link.checkout.quelle));
698
+ var standardRef = STANDARD_REF[quelleKurz(ei.link.checkout.quelle)];
699
+ if (ei.link.checkout.ref && standardRef && ei.link.checkout.ref !== standardRef) {
700
+ qz.appendChild(e('span', 'ref', '@' + ei.link.checkout.ref));
701
+ }
702
+ } else {
703
+ qz.appendChild(e('span', 'mono', pfadKurz(ei.checkoutPfad)));
704
+ }
705
+ var grund = z.gruende[name];
706
+ if (grund && grund.art === 'zeiger') qz.appendChild(e('span', 'grundmarke', 'über Zeiger'));
707
+ li.appendChild(qz);
708
+
709
+ // Aus einem Plugin verlinkt: kein Entfernen-Knopf, das geht nur über das Plugin — der
710
+ // Grund erklärt hier zugleich, warum der Knopf fehlt.
711
+ if (grund && grund.art === 'plugin') {
712
+ li.appendChild(e('span', 'kein-weg', 'nur über Plugin „' + grund.plugin.split('@')[0] + '“'));
713
+ } else {
714
+ var weg = e('button', 'knopf leise klein', 'Entfernen');
715
+ weg.type = 'button';
716
+ weg.setAttribute('aria-label', 'Link auf ' + name + ' entfernen');
717
+ weg.addEventListener('click', function () { frageEntfernen(name, z.ziel, bundle ? bundle.name : null); });
718
+ li.appendChild(weg);
719
+ }
720
+ return li;
721
+ }
722
+
723
+ /** Ein installiertes Bundle als Gruppe über seinen Skill-Zeilen (E69, F68): Kopf mit Name, Quelle
724
+ und „Entfernen" für das Ganze, darunter die Zeilen seiner Mitglieder. */
725
+ function bundleGruppe(b, z, eintraege) {
726
+ var gruppe = e('div', 'bundlegruppe');
727
+ var kopf = e('div', 'bundlekopf');
728
+ var titel = e('span', 'bundletitel');
729
+ titel.appendChild(e('span', 'etikett', 'Bundle'));
730
+ titel.appendChild(e('b', 'bundlename', b.name));
731
+ kopf.appendChild(titel);
732
+ var quelleEl = e('span', 'mono bundlequelle', b.quelleName);
733
+ quelleEl.title = b.quelleText;
734
+ kopf.appendChild(quelleEl);
735
+ kopf.appendChild(e('span', 'bundlezahl', zahlwort(b.skills.length, 'Skill', 'Skills')));
736
+ var weg = e('button', 'knopf leise klein', 'Entfernen');
737
+ weg.type = 'button';
738
+ weg.setAttribute('aria-label', 'Bundle ' + b.name + ' aus ' + zielTitel(z) + ' entfernen');
739
+ weg.addEventListener('click', function () { frageBundleEntfernen(b, z.ziel); });
740
+ kopf.appendChild(weg);
741
+ gruppe.appendChild(kopf);
742
+ var ul = e('ul', 'linkliste');
743
+ eintraege.forEach(function (ei) { ul.appendChild(linkZeile(ei, z, b)); });
744
+ gruppe.appendChild(ul);
745
+ return gruppe;
746
+ }
747
+
748
+ /** Ein Bundle als einzelner Streifen (Chip „Alle" einer Verzeichnis-Karte, E69/E53): Harness-
749
+ Zeichen, Name, seine Mitglieder als Text, Entfernen. Ohne eigene Tabelle. */
750
+ function bundleStreifen(b, z) {
751
+ var streifen = e('div', 'bundlegruppe');
752
+ var kopf = e('div', 'bundlekopf');
753
+ var titel = e('span', 'bundletitel');
754
+ titel.appendChild(harnessZeichen(z.ziel.harness));
755
+ titel.appendChild(e('span', 'etikett', 'Bundle'));
756
+ titel.appendChild(e('b', 'bundlename', b.name));
757
+ kopf.appendChild(titel);
758
+ kopf.appendChild(e('span', 'bundlemitglieder', b.skills.join(', ')));
759
+ var weg = e('button', 'knopf leise klein', 'Entfernen');
760
+ weg.type = 'button';
761
+ weg.setAttribute('aria-label', 'Bundle ' + b.name + ' aus ' + zielTitel(z) + ' entfernen');
762
+ weg.addEventListener('click', function () { frageBundleEntfernen(b, z.ziel); });
763
+ kopf.appendChild(weg);
764
+ streifen.appendChild(kopf);
765
+ return streifen;
766
+ }
767
+
655
768
  function linkTeil(z) {
656
769
  var teil = document.createDocumentFragment();
657
770
  var eigene = z.eintraege.filter(function (ei) { return ei.art !== 'fremd'; });
@@ -697,63 +810,26 @@
697
810
  kopf.appendChild(e('span'));
698
811
  teil.appendChild(kopf);
699
812
 
700
- var ul = e('ul', 'linkliste');
701
- // Defekte Links (ohne Quelle) zuerst, sie brauchen Aufmerksamkeit (E45), sonst alphabetisch.
702
- eigene
813
+ // Bundles zuerst (E69): sie stehen als Gruppe über den Einzel-Zeilen, ihre Mitglieder liegen
814
+ // in der Gruppe statt in der allgemeinen Liste. Was zum Bundle gehört, sagt der Kern.
815
+ var bundles = (z.bundles || []).filter(function (b) { return b.skills.length > 0; });
816
+ var bundleVon = {};
817
+ bundles.forEach(function (b) { b.skills.forEach(function (n) { bundleVon[n] = b; }); });
818
+ bundles
703
819
  .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);
820
+ .sort(function (a, b) { return a.name.localeCompare(b.name, 'de'); })
821
+ .forEach(function (b) {
822
+ var mitglieder = eigene.filter(function (ei) {
823
+ return b.skills.indexOf(ei.art === 'link' ? ei.link.name : ei.name) > -1;
824
+ });
825
+ teil.appendChild(bundleGruppe(b, z, sortiereEintraege(mitglieder)));
755
826
  });
756
- teil.appendChild(ul);
827
+
828
+ var ul = e('ul', 'linkliste');
829
+ sortiereEintraege(
830
+ eigene.filter(function (ei) { return !bundleVon[ei.art === 'link' ? ei.link.name : ei.name]; }),
831
+ ).forEach(function (ei) { ul.appendChild(linkZeile(ei, z, null)); });
832
+ if (ul.childNodes.length > 0) teil.appendChild(ul);
757
833
 
758
834
  // MCP-Abschnitt (E61-GUI-Nachtrag): unter der Skill-Link-Tabelle, vor dem Kartenfuß mit den
759
835
  // Knöpfen, Gegenrichtung zur MCP-Ansicht.
@@ -894,6 +970,44 @@
894
970
  return knopf;
895
971
  }
896
972
 
973
+ /** Eigene Links eines Verzeichnisses über alle Harnesses gebündelt: Schlüssel Name+Quelle,
974
+ hängt ein Name je Harness aus verschiedener Quelle, gibt es je Quelle eine eigene Zeile
975
+ (E53 Pkt. 2). Einzige Zählregel für Skills: der Körper zeilt daraus, die Kartenköpfe zählen
976
+ dieselbe Struktur (E71). Ein einzelnes Ziel kommt als einelementige Liste.
977
+ Fremde Einträge bleiben draußen, defekte Links (ohne Quelle) zählen mit. */
978
+ function buendleEigene(teile) {
979
+ var zeilen = {};
980
+ var reihenfolge = [];
981
+ teile.forEach(function (z) {
982
+ z.eintraege.filter(function (ei) { return ei.art !== 'fremd'; }).forEach(function (ei) {
983
+ var defekt = ei.art === 'link-ohne-quelle';
984
+ var name = defekt ? ei.name : ei.link.name;
985
+ var quelleSchluessel = defekt ? ('defekt\u0000' + ei.checkoutPfad)
986
+ : (quelleKurz(ei.link.checkout.quelle) + '@' + ei.link.checkout.ref);
987
+ // NUL als Trenner, kein Leerzeichen: Skill-Namen dürfen Leerzeichen enthalten, Quellen-Namen
988
+ // seit E59 auch, ein lokaler Pfad ohnehin — „a b“ + „x“ und „a“ + „b x“ ergäben sonst
989
+ // denselben Schlüssel. Immer als Escape geschrieben, nie als rohes Byte im Quelltext.
990
+ var key = name + '\u0000' + quelleSchluessel;
991
+ if (!zeilen[key]) {
992
+ zeilen[key] = {
993
+ name: name,
994
+ defekt: defekt,
995
+ icon: defekt ? { art: 'lokal', pfad: ei.checkoutPfad } : ei.link.checkout.quelle,
996
+ checkoutPfad: defekt ? ei.checkoutPfad : null,
997
+ quelle: defekt ? null : ei.link.checkout.quelle,
998
+ ref: defekt ? null : ei.link.checkout.ref,
999
+ einschraenkung: defekt ? null : z.einschraenkungen[name],
1000
+ skill: defekt ? null : ei.link.skill,
1001
+ teile: [],
1002
+ };
1003
+ reihenfolge.push(key);
1004
+ }
1005
+ zeilen[key].teile.push(z);
1006
+ });
1007
+ });
1008
+ return { zeilen: zeilen, reihenfolge: reihenfolge };
1009
+ }
1010
+
897
1011
  function zielKarte(z) {
898
1012
  var karte = e('div', 'ziel');
899
1013
  var kopf = e('div', 'zielkopf');
@@ -917,8 +1031,10 @@
917
1031
  warnspan.setAttribute('aria-label', 'braucht Aufmerksamkeit');
918
1032
  kopf.appendChild(warnspan);
919
1033
  }
920
- var zahl = z.eintraege.filter(function (ei) { return ei.art !== 'fremd'; }).length;
921
- kopf.appendChild(e('span', 'zahl', zahl + (zahl === 1 ? ' Link' : ' Links')));
1034
+ // Der Kopf zählt Skills, nicht Einträge (E71), über dieselbe Bündelung, aus der der Körper zeilt.
1035
+ // Bei einem Harness fällt die Zahl mit der Zahl eigener Einträge zusammen, die Regel bleibt eine.
1036
+ var zahl = buendleEigene([z]).reihenfolge.length;
1037
+ kopf.appendChild(e('span', 'zahl', zahl + (zahl === 1 ? ' Skill' : ' Skills')));
922
1038
  var pfeil = document.createElementNS('http://www.w3.org/2000/svg', 'svg');
923
1039
  pfeil.setAttribute('viewBox', '0 0 16 16');
924
1040
  pfeil.setAttribute('class', 'akkordeonpfeil');
@@ -981,37 +1097,9 @@
981
1097
  if (beachteteAlle.length > 0) teil.appendChild(fremdKasten(beachteteAlle, true));
982
1098
  if (ignorierteAlle.length > 0) teil.appendChild(ignoriertZeile(ignorierteAlle, true));
983
1099
 
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
- });
1100
+ var gebuendelt = buendleEigene(teile);
1101
+ var zeilen = gebuendelt.zeilen;
1102
+ var reihenfolge = gebuendelt.reihenfolge;
1015
1103
 
1016
1104
  // MCPs hängen am Verzeichnis, nicht am Harness-Chip (E61-GUI-Nachtrag): der Abschnitt unter
1017
1105
  // „Alle" zeigt jedes Ziel, das an genau diesem Pfad MCP kann — dieselbe Fähigkeitsfrage wie unter
@@ -1038,6 +1126,18 @@
1038
1126
  return teil;
1039
1127
  }
1040
1128
 
1129
+ // Bundles unter „Alle" (E69): die Zeilen bleiben nach Name und Quelle gebündelt (E53), ein
1130
+ // Bundle gehört aber zu genau einem Harness — deshalb hier ein Streifen je Bundle mit seinem
1131
+ // Harness-Zeichen statt einer Umgruppierung der Tabelle. Unter dem Harness-Chip zeigt linkTeil
1132
+ // die volle Gruppe über den Zeilen.
1133
+ teile.forEach(function (z) {
1134
+ (z.bundles || [])
1135
+ .filter(function (b) { return b.skills.length > 0; })
1136
+ .slice()
1137
+ .sort(function (a, b) { return a.name.localeCompare(b.name, 'de'); })
1138
+ .forEach(function (b) { teil.appendChild(bundleStreifen(b, z)); });
1139
+ });
1140
+
1041
1141
  var kopf = e('div', 'linkkopf');
1042
1142
  kopf.appendChild(e('span'));
1043
1143
  kopf.appendChild(e('span', 'etikett', 'Skill'));
@@ -1173,10 +1273,10 @@
1173
1273
  warnspan.setAttribute('aria-label', 'braucht Aufmerksamkeit');
1174
1274
  kopf.appendChild(warnspan);
1175
1275
  }
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')));
1276
+ // Skills statt Einträge (E71): eine Summe über die Harness-Abschnitte zählte jeden Skill so oft,
1277
+ // wie ihn Harnesses am selben Verzeichnis führen — der Körper bündelt sie zu einer Zeile.
1278
+ var gesamtZahl = buendleEigene(teile).reihenfolge.length;
1279
+ kopf.appendChild(e('span', 'zahl', gesamtZahl + (gesamtZahl === 1 ? ' Skill' : ' Skills')));
1180
1280
  var pfeil = document.createElementNS('http://www.w3.org/2000/svg', 'svg');
1181
1281
  pfeil.setAttribute('viewBox', '0 0 16 16');
1182
1282
  pfeil.setAttribute('class', 'akkordeonpfeil');
@@ -1326,10 +1426,24 @@
1326
1426
  // entfernenKontext.eintraege: [{harness, ort}], immer eines bei frageEntfernen (Einzel-Ziel),
1327
1427
  // mehrere bei frageEntfernenAlle (E53 Pkt. 2, „Alle"-Zeile über mehrere Harnesses).
1328
1428
  var entfernenKontext = null;
1329
- function frageEntfernen(name, ziel) {
1330
- entfernenKontext = { name: name, eintraege: [{ harness: ziel.harness, ort: ziel.ort }] };
1429
+ /** `bundleName` gesetzt heißt: der Link gehört zu einem Bundle, das Entfernen zerbricht es (E69). */
1430
+ function frageEntfernen(name, ziel, bundleName) {
1431
+ entfernenKontext = { art: 'link', name: name, eintraege: [{ harness: ziel.harness, ort: ziel.ort }] };
1331
1432
  $('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.';
1433
+ var text = 'Der Link in ' + zielTitel({ ziel: ziel }) + ' verschwindet. Der Checkout bleibt liegen, bis kein Link mehr darauf zeigt.';
1434
+ if (bundleName) {
1435
+ text += ' Damit zerbricht das Bundle „' + bundleName + '“ hier: seine übrigen Links bleiben liegen, aber nicht mehr als Bundle.';
1436
+ }
1437
+ $('dx-text').textContent = text;
1438
+ zeige($('d-entfernen'));
1439
+ }
1440
+
1441
+ /** Entfernen des ganzen Bundles an einem Ziel (E69): alle seine Links auf einmal. */
1442
+ function frageBundleEntfernen(b, ziel) {
1443
+ entfernenKontext = { art: 'bundle', bundle: b.name, ziel: ziel };
1444
+ $('dx-titel').textContent = 'Bundle ' + b.name + ' entfernen?';
1445
+ $('dx-text').textContent = zahlwort(b.skills.length, 'Link', 'Links') + ' aus ' + zielTitel({ ziel: ziel })
1446
+ + ' verschwinden: ' + b.skills.join(', ') + '. Die Checkouts bleiben liegen, bis kein Link mehr darauf zeigt.';
1333
1447
  zeige($('d-entfernen'));
1334
1448
  }
1335
1449
  /** Entfernen einer „Alle"-Zeile (E53 Pkt. 2): entfernbareZiele sind die Ziele (voller z.ziel), in
@@ -1337,6 +1451,7 @@
1337
1451
  und liegen bleibt. Der Dialog nennt beides. */
1338
1452
  function frageEntfernenAlle(name, pfad, entfernbareZiele, nichtEntfernbareHarnesse) {
1339
1453
  entfernenKontext = {
1454
+ art: 'link',
1340
1455
  name: name,
1341
1456
  eintraege: entfernbareZiele.map(function (z) { return { harness: z.ziel.harness, ort: z.ziel.ort }; }),
1342
1457
  };
@@ -1353,6 +1468,19 @@
1353
1468
  nachDialog($('d-entfernen'), async function (wert) {
1354
1469
  if (wert !== 'ja' || entfernenKontext === null) return;
1355
1470
  var kontext = entfernenKontext;
1471
+ // Bundle als Ganzes (E69): ein Aufruf, der Kern nimmt alle Links des Bundles weg.
1472
+ if (kontext.art === 'bundle') {
1473
+ var wegB = await rufe(function () {
1474
+ return window.hmgr.bundleEntfernen({
1475
+ bundle: kontext.bundle,
1476
+ harness: kontext.ziel.harness,
1477
+ ort: kontext.ziel.ort,
1478
+ });
1479
+ });
1480
+ await ladeAlles();
1481
+ melde('Bundle ' + kontext.bundle + ' entfernt, ' + zahlwort(wegB.entfernt.length, 'Link', 'Links') + ' weniger.');
1482
+ return;
1483
+ }
1356
1484
  // Ein Fehlschlag in einem Harness hält die übrigen nicht auf: rufe hat ihn schon gemeldet, die
1357
1485
  // Schleife läuft weiter, und die Liste wird danach in jedem Fall neu geladen. Sonst stünde nach
1358
1486
  // einem Fehler die halb erledigte Arbeit unsichtbar in einer veralteten Liste.
@@ -1663,6 +1791,14 @@
1663
1791
  return zeilen;
1664
1792
  }
1665
1793
 
1794
+ /** ISO-Zeitpunkt als deutsches Datum (E70). Ungültiges oder fehlendes Datum ergibt null. */
1795
+ function datumDeutsch(iso) {
1796
+ if (!iso) return null;
1797
+ var d = new Date(iso);
1798
+ if (isNaN(d.getTime())) return null;
1799
+ return d.toLocaleDateString('de-DE', { day: '2-digit', month: '2-digit', year: 'numeric' });
1800
+ }
1801
+
1666
1802
  function baueSkills() {
1667
1803
  var liste = $('katalog');
1668
1804
  liste.textContent = '';
@@ -1686,12 +1822,17 @@
1686
1822
  // title. Fehlt die Quelle in der Liste (Katalog und Quellen kämen auseinander), bleibt es
1687
1823
  // beim reinen Text — angeschrieben ist die Zeile so oder so.
1688
1824
  var herkunft = e('p', 'herkunft mono');
1825
+ // Das Änderungsdatum steht hinter dem Ref (E70, F70=O1): der jüngste Commit, der den
1826
+ // Skill-Ordner berührt. Nicht der Pull-Zeitpunkt — der steht als „Zuletzt geprüft" in der
1827
+ // Update-Ansicht und wäre für alle Skills einer Quelle derselbe.
1828
+ var geaendert = datumDeutsch(skill.geaendert);
1829
+ var nachRef = ' · Ref ' + skill.ref + (geaendert ? ' · geändert ' + geaendert : '');
1689
1830
  if (herkunftQuelle) {
1690
1831
  herkunft.appendChild(quelleLinkElement(herkunftQuelle));
1691
- herkunft.appendChild(e('b', null, ' · Ref ' + skill.ref));
1832
+ herkunft.appendChild(e('b', null, nachRef));
1692
1833
  } else {
1693
1834
  herkunft.title = skill.quelleText;
1694
- herkunft.appendChild(e('b', null, skill.quelleName + ' · Ref ' + skill.ref));
1835
+ herkunft.appendChild(e('b', null, skill.quelleName + nachRef));
1695
1836
  }
1696
1837
  links.appendChild(herkunft);
1697
1838
 
@@ -1840,7 +1981,8 @@
1840
1981
  Skill nur aus, wenn er in ALLEN beteiligten Harnesses hängt, und verlinkt je Harness, wo er fehlt. */
1841
1982
  var skillsKontext = null;
1842
1983
  function dskBilanzAktualisieren() {
1843
- var n = $('dsk-liste').querySelectorAll('input:checked').length;
1984
+ var n = $('dsk-liste').querySelectorAll('input:checked').length
1985
+ + $('dsk-bundles').querySelectorAll('input:checked').length;
1844
1986
  $('dsk-bilanz').textContent = zahlwort(n, 'ausgewählt', 'ausgewählt');
1845
1987
  }
1846
1988
  function filtereDskListe() {
@@ -1849,6 +1991,60 @@
1849
1991
  var skill = SKILLS[+label.dataset.index];
1850
1992
  label.hidden = !!suche && skill.name.toLowerCase().indexOf(suche) === -1;
1851
1993
  });
1994
+ // Die Suche gilt auch für Bundles (E69): sonst bliebe die Sektion bei jeder Eingabe voll stehen.
1995
+ document.querySelectorAll('#dsk-bundles .wahlzeile').forEach(function (label) {
1996
+ var bundle = BUNDLES[+label.dataset.index];
1997
+ label.hidden = !!suche && bundle.name.toLowerCase().indexOf(suche) === -1;
1998
+ });
1999
+ }
2000
+
2001
+ /**
2002
+ * Bundle-Sektion des Dialogs (E69, F68): je Bundle eine Wahlzeile mit Beschreibung, Zahl seiner
2003
+ * Mitglieder und Herkunft. `installiertIn(bundle)` sagt, in welchen der angesteuerten Ziele es
2004
+ * schon hängt — hängt es überall, ist die Zeile ausgegraut, wie bei einem Skill.
2005
+ */
2006
+ function fuelleBundleSektion(installiertIn, zieleZahl) {
2007
+ var box = $('dsk-bundles');
2008
+ box.textContent = '';
2009
+ BUNDLES.forEach(function (bundle, i) {
2010
+ var schon = installiertIn(bundle);
2011
+ var ueberall = schon >= zieleZahl;
2012
+ var label = e('label', 'wahlzeile quellfarbe');
2013
+ label.dataset.index = i;
2014
+ var idx = quellenIndex(bundle.quelleText);
2015
+ label.style.borderLeftColor = 'var(--q' + (idx + 1) + ')';
2016
+ var input = document.createElement('input');
2017
+ input.type = 'checkbox';
2018
+ input.value = String(i);
2019
+ input.disabled = ueberall;
2020
+ input.addEventListener('change', dskBilanzAktualisieren);
2021
+ label.appendChild(input);
2022
+ var txt = e('span');
2023
+ txt.appendChild(e('b', null, bundle.name));
2024
+ txt.appendChild(e('span', 'notiz', bundle.beschreibung));
2025
+ var herkunft = e('span', 'mono', zahlwort(bundle.mitglieder, 'Skill', 'Skills')
2026
+ + ' aus ' + zahlwort(bundle.quellen.length, 'Quelle', 'Quellen') + ' · ' + bundle.quelleName);
2027
+ herkunft.title = bundle.quellen.join('\n');
2028
+ txt.appendChild(herkunft);
2029
+ if (ueberall) txt.appendChild(e('span', 'notiz', zieleZahl > 1 ? 'Hängt schon überall.' : 'Hängt hier schon.'));
2030
+ else if (schon > 0) txt.appendChild(e('span', 'notiz', 'Hängt schon in ' + schon + ' von ' + zieleZahl + ' Harnesses.'));
2031
+ label.appendChild(txt);
2032
+ box.appendChild(label);
2033
+ });
2034
+ $('dsk-bundlebox').hidden = BUNDLES.length === 0 && BUNDLES_KAPUTT.length === 0;
2035
+ // Eine Definition, die sich nicht lesen ließ, wird genannt statt verschluckt (E69).
2036
+ var fehlerZeile = $('dsk-bundlefehler');
2037
+ fehlerZeile.hidden = BUNDLES_KAPUTT.length === 0;
2038
+ fehlerZeile.textContent = BUNDLES_KAPUTT.map(function (k) {
2039
+ return k.quelleName + ': ' + k.datei + ' — ' + k.fehler;
2040
+ }).join('\n');
2041
+ }
2042
+
2043
+ /** Wie oft ein Bundle in den angesteuerten Zielen schon installiert ist (Name plus Quelle, E69). */
2044
+ function bundleHaengtIn(teile, bundle) {
2045
+ return teile.filter(function (z) {
2046
+ return (z.bundles || []).some(function (b) { return b.name === bundle.name && b.quelleText === bundle.quelleText; });
2047
+ }).length;
1852
2048
  }
1853
2049
  $('dsk-suche').addEventListener('input', filtereDskListe);
1854
2050
 
@@ -1883,6 +2079,7 @@
1883
2079
  label.appendChild(txt);
1884
2080
  box.appendChild(label);
1885
2081
  });
2082
+ fuelleBundleSektion(function (bundle) { return bundleHaengtIn([z], bundle); }, 1);
1886
2083
  dskBilanzAktualisieren();
1887
2084
  zeige($('d-skills'));
1888
2085
  $('dsk-suche').focus();
@@ -1927,6 +2124,7 @@
1927
2124
  label.appendChild(txt);
1928
2125
  box.appendChild(label);
1929
2126
  });
2127
+ fuelleBundleSektion(function (bundle) { return bundleHaengtIn(teile, bundle); }, teile.length);
1930
2128
  dskBilanzAktualisieren();
1931
2129
  zeige($('d-skills'));
1932
2130
  $('dsk-suche').focus();
@@ -1934,7 +2132,8 @@
1934
2132
 
1935
2133
  $('d-skills').addEventListener('submit', function (ev) {
1936
2134
  if (ev.submitter && ev.submitter.value === 'los') {
1937
- var gewaehlt = Array.from($('dsk-liste').querySelectorAll('input:checked'));
2135
+ var gewaehlt = Array.from($('dsk-liste').querySelectorAll('input:checked'))
2136
+ .concat(Array.from($('dsk-bundles').querySelectorAll('input:checked')));
1938
2137
  if (gewaehlt.length === 0) ev.preventDefault();
1939
2138
  }
1940
2139
  });
@@ -1943,6 +2142,7 @@
1943
2142
  if (wert !== 'los' || skillsKontext === null) return;
1944
2143
  var kontext = skillsKontext;
1945
2144
  var indizes = Array.from($('dsk-liste').querySelectorAll('input:checked')).map(function (i) { return +i.value; });
2145
+ var bundleIndizes = Array.from($('dsk-bundles').querySelectorAll('input:checked')).map(function (i) { return +i.value; });
1946
2146
 
1947
2147
  if (kontext.modus === 'einzeln') {
1948
2148
  var z = kontext.z;
@@ -1952,6 +2152,26 @@
1952
2152
  // Der zweite gehört daher als Fehler in die Bilanz, sonst meldet der Dialog Links, die es nicht gibt.
1953
2153
  var vergeben = [];
1954
2154
  var hinweiseZiel = [];
2155
+ // Bundles zuerst (E69): sie wirken als Ganzes und bringen ihre Mitglieder mit, ein danach
2156
+ // einzeln gewählter Skill gleichen Namens würde den eben gelegten Link stumm ersetzen.
2157
+ for (var bi = 0; bi < bundleIndizes.length; bi++) {
2158
+ var bundle = BUNDLES[bundleIndizes[bi]];
2159
+ try {
2160
+ var erg = await rufe(function () {
2161
+ return window.hmgr.bundleInstallieren({
2162
+ bundle: bundle.name,
2163
+ harness: z.ziel.harness,
2164
+ ort: z.ziel.ort,
2165
+ quelle: bundle.quelleText,
2166
+ });
2167
+ });
2168
+ hinweiseZiel.push(erg.hinweis);
2169
+ erg.gelegt.concat(erg.uebernommen).forEach(function (n) { vergeben.push(n); });
2170
+ ergebnisZeilen.push({ ok: true, bundle: bundle, erg: erg });
2171
+ } catch (fehler) {
2172
+ ergebnisZeilen.push({ ok: false, bundle: bundle, fehler: fehler.message });
2173
+ }
2174
+ }
1955
2175
  for (var n = 0; n < indizes.length; n++) {
1956
2176
  var skill = SKILLS[indizes[n]];
1957
2177
  if (vergeben.indexOf(skill.name) > -1) {
@@ -1977,18 +2197,30 @@
1977
2197
  // Arbeitsverzeichnis ist das die gebündelte Verzeichnis-Karte, nicht das Ziel allein (E49).
1978
2198
  ZIEL_OFFEN[kartenKey(z.ziel)] = true;
1979
2199
  await ladeAlles();
1980
- var erfolge = ergebnisZeilen.filter(function (r) { return r.ok; }).length;
2200
+ // Ein Bundle zählt als eines, egal wie viele Links es mitbringt: die Zeile darunter nennt sie.
2201
+ var erfolge = ergebnisZeilen.filter(function (r) { return r.ok && !r.bundle; }).length;
2202
+ var erfolgeBundle = ergebnisZeilen.filter(function (r) { return r.ok && r.bundle; }).length;
1981
2203
  $('de-titel').textContent = 'Verlinkt';
1982
- $('de-text').textContent = zahlwort(erfolge, 'Skill hängt', 'Skills hängen') + ' jetzt in ' + zielTitel(z) + '.';
2204
+ $('de-text').textContent = (erfolgeBundle > 0 ? zahlwort(erfolgeBundle, 'Bundle', 'Bundles') + ' und ' : '')
2205
+ + zahlwort(erfolge, 'Skill hängt', 'Skills hängen') + ' jetzt in ' + zielTitel(z) + '.';
1983
2206
  setzeErfolgHinweise(hinweiseZiel);
1984
2207
  var ul = $('de-liste');
1985
2208
  ul.textContent = '';
1986
2209
  ergebnisZeilen.forEach(function (r) {
1987
2210
  var li = e('li');
1988
- li.appendChild(e('span', r.ok ? 'ok' : 'aus', r.ok ? 'Link' : 'Fehler'));
2211
+ li.appendChild(e('span', r.ok ? 'ok' : 'aus', r.ok ? (r.bundle ? 'Bundle' : 'Link') : 'Fehler'));
1989
2212
  var s = e('span');
1990
- s.appendChild(e('b', null, r.skill.name));
2213
+ s.appendChild(e('b', null, r.bundle ? r.bundle.name : r.skill.name));
1991
2214
  if (!r.ok) s.appendChild(e('span', 'mono', r.fehler));
2215
+ else if (r.bundle) {
2216
+ var teileText = r.erg.gelegt.concat(r.erg.uebernommen).join(', ');
2217
+ if (r.erg.neueQuellen.length > 0) {
2218
+ // Neue Quellen (F66) gehören genannt: hmgr hat sie beim Installieren selbst angelegt.
2219
+ teileText += ' · neue ' + zahlwort(r.erg.neueQuellen.length, 'Quelle', 'Quellen') + ': '
2220
+ + r.erg.neueQuellen.join(', ');
2221
+ }
2222
+ s.appendChild(e('span', 'mono', teileText));
2223
+ }
1992
2224
  li.appendChild(s);
1993
2225
  ul.appendChild(li);
1994
2226
  });
@@ -2007,6 +2239,32 @@
2007
2239
  // den eben gelegten Link sonst stumm.
2008
2240
  var vergebenJeHarness = {};
2009
2241
  var hinweiseAlle = [];
2242
+ // Bundles zuerst, je Harness, in dem sie noch nicht hängen (E69): ein Bundle gehört zu genau
2243
+ // einem Ziel, unter „Alle" also einmal je beteiligtem Harness.
2244
+ for (var bj = 0; bj < bundleIndizes.length; bj++) {
2245
+ var bundleAlle = BUNDLES[bundleIndizes[bj]];
2246
+ for (var bt = 0; bt < teile.length; bt++) {
2247
+ var zb = teile[bt];
2248
+ if (bundleHaengtIn([zb], bundleAlle) > 0) continue;
2249
+ try {
2250
+ var ergAlle = await rufe(function () {
2251
+ return window.hmgr.bundleInstallieren({
2252
+ bundle: bundleAlle.name,
2253
+ harness: zb.ziel.harness,
2254
+ ort: zb.ziel.ort,
2255
+ quelle: bundleAlle.quelleText,
2256
+ });
2257
+ });
2258
+ hinweiseAlle.push(ergAlle.hinweis);
2259
+ ergAlle.gelegt.concat(ergAlle.uebernommen).forEach(function (n) {
2260
+ vergebenJeHarness[zb.ziel.harness + '\u0000' + n] = true;
2261
+ });
2262
+ zeilenErgebnis.push({ ok: true, bundle: bundleAlle, harness: zb.ziel.harness, erg: ergAlle });
2263
+ } catch (fehler) {
2264
+ zeilenErgebnis.push({ ok: false, bundle: bundleAlle, harness: zb.ziel.harness, fehler: fehler.message });
2265
+ }
2266
+ }
2267
+ }
2010
2268
  for (var m = 0; m < indizes.length; m++) {
2011
2269
  var sk = SKILLS[indizes[m]];
2012
2270
  for (var t = 0; t < teile.length; t++) {
@@ -2089,15 +2347,34 @@
2089
2347
  if (c.aktualisierung.ergebnis === 'aktualisiert') quellen[quelleKurz(c.checkout.quelle)] = true;
2090
2348
  });
2091
2349
  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".
2352
+ var geaenderteSkills = {};
2353
+ ergebnis.checkouts.forEach(function (c) {
2354
+ (c.geaenderteSkills || []).forEach(function (name) { geaenderteSkills[name] = true; });
2355
+ });
2356
+ 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.
2359
+ var bundlesNeu = (ergebnis.bundles || []).filter(function (b) { return b.ergebnis === 'aktualisiert'; }).length;
2092
2360
  var fehlgeschlagen = ergebnis.checkouts.filter(function (c) { return c.aktualisierung.ergebnis === 'fehlgeschlagen'; }).length;
2093
2361
  var gesamt = ergebnis.checkouts.length;
2094
2362
  standFehlerZustand = fehlgeschlagen === 0 ? 'keine' : fehlgeschlagen === gesamt ? 'alle' : 'gemischt';
2095
2363
  // 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();
2364
+ if ((neue === 0 && bundlesNeu === 0) || !automatisch || !$('ansicht-update').hidden) return standRuhig();
2097
2365
  var p = $('standtext');
2098
2366
  p.textContent = '';
2099
- p.appendChild(e('b', null, zahlwort(neue, 'Quelle', 'Quellen')));
2100
- p.appendChild(document.createTextNode(' mit Neuem'));
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'));
2372
+ } 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'));
2377
+ }
2101
2378
  $('standknopf').classList.add('neu');
2102
2379
  $('standknopf').textContent = 'Ansehen';
2103
2380
  }
@@ -2184,7 +2461,11 @@
2184
2461
  : a.ergebnis === 'fehlgeschlagen' ? 'fehlgeschlagen (' + a.fehler + ')'
2185
2462
  : 'übersprungen (lokale Änderung)';
2186
2463
  li.appendChild(e('span', 'marke-' + art, text));
2187
- li.appendChild(quelleSpan('pfad', c.checkout.quelle, ' @ ' + c.checkout.ref));
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(', ') : '')));
2188
2469
  cul.appendChild(li);
2189
2470
  });
2190
2471
  checkoutBlock.appendChild(cul);
@@ -2204,6 +2485,33 @@
2204
2485
  box.appendChild(pluginBlock);
2205
2486
  }
2206
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);
2513
+ }
2514
+
2207
2515
  if (ergebnis.aufraeumen.verwaiste.length > 0) {
2208
2516
  var raeumBlock = e('div', 'block entfernt');
2209
2517
  raeumBlock.appendChild(e('span', 'etikett', 'Verwaiste Checkouts'));