iobroker.alias-workbench 0.9.8 → 0.9.10

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.
@@ -92,6 +92,15 @@ function ausVorlage(punktId, neuKanal) {
92
92
 
93
93
  /* Stufe 3: gleicher Punktname irgendwo unter dem neuen Geraet, oder
94
94
  gleiche Rolle. Bewusst schwach - deshalb nur als Vorschlag. */
95
+ /* Und WORUEBER geraten wurde, wird mitgegeben.
96
+
97
+ „vermutet" allein sagt nur, dass die Zuordnung schwach ist - nicht,
98
+ woran sie haengt. Ueber den Namen geraten heisst: irgendein Punkt
99
+ heisst hinten gleich; das kann weit danebenliegen (ein Fensterkontakt
100
+ `…1.STATE` landete so auf `…tele.STATE`, dem Telemetrie-JSON einer
101
+ Tasmota). Ueber die Rolle geraten ist etwas anderes und muss anders
102
+ geprueft werden. Der Tooltip an der Marke sagt es jetzt (Ricardo,
103
+ 12.09.2026). */
95
104
  function geraten(altQuelle, neuKanal) {
96
105
  var name = altQuelle.split('.').pop();
97
106
  var kandidaten = kindZustaende(neuKanal);
@@ -99,7 +108,7 @@ function geraten(altQuelle, neuKanal) {
99
108
  kandidaten.forEach(function (id) {
100
109
  if (!treffer && id.split('.').pop() === name) { treffer = id; }
101
110
  });
102
- if (treffer) { return treffer; }
111
+ if (treffer) { return { id: treffer, ueber: 'name' }; }
103
112
  var altO = S.objects[altQuelle];
104
113
  var rolle = altO && altO.common && altO.common.role;
105
114
  if (rolle) {
@@ -108,7 +117,7 @@ function geraten(altQuelle, neuKanal) {
108
117
  if (!treffer && o && o.common && o.common.role === rolle) { treffer = id; }
109
118
  });
110
119
  }
111
- return treffer;
120
+ return treffer ? { id: treffer, ueber: 'rolle', rolle: rolle } : null;
112
121
  }
113
122
 
114
123
  /* Die Zuordnungstabelle: je Alias-Punkt, was heute gilt und was danach
@@ -158,10 +167,14 @@ export function tauschPlan(kanal, neuKanal) {
158
167
  /* 3. geraten */
159
168
  if (!zeile.neuR && zeile.altR) {
160
169
  var g = geraten(zeile.altR, neuKanal);
161
- if (g) { zeile.neuR = g; zeile.stufe = 3; }
170
+ if (g) {
171
+ zeile.neuR = g.id; zeile.stufe = 3;
172
+ zeile.geratenUeber = g.ueber; zeile.geratenRolle = g.rolle || '';
173
+ }
162
174
  }
163
175
  if (!zeile.neuW && zeile.altW) {
164
- var gw = geraten(zeile.altW, neuKanal);
176
+ var gwT = geraten(zeile.altW, neuKanal);
177
+ var gw = gwT && gwT.id;
165
178
  /* Was hier geraten wird, muss ein anderer Punkt sein als die
166
179
  Lesequelle. Bei getrennten Quellen heissen Melder und Befehl oft
167
180
  gleich - `stat.POWER` und `cmnd.POWER` -, und das Raten greift
@@ -172,6 +185,21 @@ export function tauschPlan(kanal, neuKanal) {
172
185
  (gemessen 09.09.2026). Lieber kein Treffer als dieser. */
173
186
  if (gw && gw !== zeile.neuR) { zeile.neuW = gw; }
174
187
  }
188
+ /* Bei getrennten Quellen darf das Schreibziel nie die Lesequelle
189
+ sein - egal, aus welcher der drei Stufen es kam.
190
+
191
+ Beim Raten stand diese Regel schon (siehe oben), beim PFADTREFFER
192
+ fehlte sie: `suchePfad` probiert als letzte Form den blossen
193
+ Punktnamen, und `cmnd.POWER` findet so `…Stumm.stat.POWER` - den
194
+ Melder. Sichtbar wird es, wenn man beim Tauschen nicht das Geraet,
195
+ sondern einen Unterknoten waehlt (`…Stumm.stat` statt `…Stumm`):
196
+ dann verschwand der rote Hinweis, der Knopf wurde frei, und
197
+ geschrieben haette der Alias auf seine eigene Lesequelle - genau
198
+ der Fehler, den W20 abschaffen sollte (Ricardo, 12.09.2026).
199
+
200
+ Danach steht `neuW` leer, und die Sperre aus W20 greift: der
201
+ Nutzer entscheidet, ob der Punkt entfernt wird. */
202
+ if (!zeile.einfach && zeile.neuW && zeile.neuW === zeile.neuR) { zeile.neuW = ''; }
175
203
  /* Bei einfacher Quelle gilt die Lesequelle fuer beides. */
176
204
  if (zeile.einfach && zeile.neuR) { zeile.neuW = zeile.neuR; }
177
205
  return zeile;
@@ -339,6 +367,25 @@ function zeichneAuswahlbaum(host, filter, beiWahl) {
339
367
  if (!host.childNodes.length) {
340
368
  host.appendChild(el('div', 'empty', tr('swap.noDevice')));
341
369
  }
370
+
371
+ /* Die Liste bleibt stehen, wo sie stand.
372
+
373
+ Beim Aufklappen fiel das nicht auf: derselbe Behaelter, nur neu
374
+ gefuellt, und `scrollTop` ueberlebt das. Beim WAEHLEN schon -
375
+ `zeichneTausch` baut den ganzen Dialog neu, der Baum ist danach ein
376
+ anderes Element und faengt bei 0 an. Wer weit unten ein Geraet
377
+ anklickte, sah danach den Anfang der Liste und seinen eben
378
+ gewaehlten Eintrag gar nicht mehr (Ricardo, 12.09.2026, an einer
379
+ Liste mit 42 Zeilen: scrollTop 700 -> 0).
380
+
381
+ Gemerkt wird an `S`, nicht am Element: das Element gibt es beim
382
+ naechsten Zeichnen nicht mehr. `block: 'nearest'` schiebt nur, wenn
383
+ die Zeile wirklich draussen ist - sonst springt die Liste bei jedem
384
+ Klick ein Stueck, und das waere die naechste Unruhe. */
385
+ if (S.tauschScroll) { host.scrollTop = S.tauschScroll; }
386
+ var gewaehlteZeile = host.querySelector('.node.sel');
387
+ if (gewaehlteZeile) { gewaehlteZeile.scrollIntoView({ block: 'nearest' }); }
388
+ host.addEventListener('scroll', function () { S.tauschScroll = host.scrollTop; });
342
389
  }
343
390
 
344
391
  export function zeigeTausch() {
@@ -355,6 +402,9 @@ export function zeigeTausch() {
355
402
  schlimmsten Fall genau dann, wenn im Feld noch das eben getauschte,
356
403
  inzwischen geloeschte Geraet stand (W13, gemessen 26.08.2026). */
357
404
  S.tauschFilter = '';
405
+ /* Und die Liste faengt oben an - sonst startet der naechste Tausch
406
+ dort, wo der letzte aufgehoert hat. */
407
+ S.tauschScroll = 0;
358
408
  var b = $('#btn-swap-go');
359
409
  if (b) { b.hidden = false; b.textContent = tr('swap.now'); }
360
410
  zeichneTausch();
@@ -420,6 +470,10 @@ function zeichneTausch() {
420
470
  zeichneAuswahlbaum(baumHost, S.tauschFilter, gewaehlt);
421
471
  feld.addEventListener('input', function () {
422
472
  S.tauschFilter = feld.value;
473
+ /* Andere Suche, andere Liste - die alte Scrollhoehe passt nicht mehr
474
+ und zeigte sonst ins Leere. */
475
+ S.tauschScroll = 0;
476
+ baumHost.scrollTop = 0;
423
477
  zeichneAuswahlbaum(baumHost, S.tauschFilter, gewaehlt);
424
478
  });
425
479
 
@@ -434,6 +488,40 @@ function zeichneTausch() {
434
488
  /* Die Zuordnung, Zeile fuer Zeile. */
435
489
  if (!S.tauschPlan) { S.tauschPlan = tauschPlan(kanal, S.tauschNeu); }
436
490
  var plan = S.tauschPlan;
491
+
492
+ /* Steht die Wahl eine Ebene zu tief?
493
+
494
+ Der Baum laesst jeden Knoten waehlen, unter dem Datenpunkte haengen -
495
+ absichtlich, denn welche Ebene beim fremden Adapter das Geraet ist,
496
+ kann die Werkbank nicht wissen; bei einem flachen Adapter ist der
497
+ Kanal die richtige (Kommentar in `tauschPlan`). Bei MQTT sind
498
+ `stat`, `tele` und `cmnd` aber Themenordner, keine Geraete, und wer
499
+ dort landet, bekommt schlechtere Treffer: am RGB-Deckenlicht fanden
500
+ ueber das Geraet alle sieben Punkte ein Ziel, ueber `…Licht.tele`
501
+ nur fuenf - die beiden aus `stat` gar nichts (12.09.2026).
502
+
503
+ Verboten wird nichts. Aber die Werkbank kann nachrechnen, was eine
504
+ Ebene hoeher herauskaeme, und es sagen - sonst hakt man zwei Punkte
505
+ zum Entfernen an und opfert seinen Schalter, statt den Knoten
506
+ darueber zu nehmen (Ricardos Frage, 12.09.2026). */
507
+ var eltern = S.tauschNeu.split('.').slice(0, -1).join('.');
508
+ if (eltern.split('.').length > 2 && S.tauschNeu !== alt) {
509
+ var trefferHier = plan.filter(function (z) { return z.neuR; }).length;
510
+ var planOben = tauschPlan(kanal, eltern);
511
+ var trefferOben = planOben.filter(function (z) { return z.neuR; }).length;
512
+ if (trefferOben > trefferHier) {
513
+ var eb = el('div', 'aside w');
514
+ eb.style.borderLeftColor = 'var(--warn)';
515
+ eb.appendChild(el('b', null, tr('swap.higherLevel',
516
+ eltern, trefferOben, plan.length, trefferHier)));
517
+ var knopf = el('button', 'btn mini wichtig', tr('swap.goHigher'));
518
+ knopf.style.marginTop = '5px';
519
+ knopf.addEventListener('click', function () { gewaehlt(eltern); });
520
+ eb.appendChild(el('div', null, ''));
521
+ eb.appendChild(knopf);
522
+ body.appendChild(eb);
523
+ }
524
+ }
437
525
  var k = el('div', 'card');
438
526
  k.style.marginTop = '11px';
439
527
  var ch = el('div', 'ch');
@@ -460,9 +548,49 @@ function zeichneTausch() {
460
548
  && (z.einfach || (z.neuW || '') === (z.altW || ''));
461
549
  var nm = el('span', 'sn', z.name);
462
550
  if (gleich) { nm.appendChild(el('span', 'rat', tr('swap.unchanged'))); }
463
- else if (z.stufe === 3) { nm.appendChild(el('span', 'rat', tr('guess.mark'))); }
551
+ else if (z.stufe === 3) {
552
+ /* Die Marke sagt jetzt auch, WORUEBER geraten wurde - ueber den
553
+ blossen Punktnamen oder ueber die Rolle. Das sind zwei sehr
554
+ verschiedene Vermutungen, und sie pruefen sich verschieden. */
555
+ var rm = el('span', 'rat', tr('guess.mark'));
556
+ rm.title = z.geratenUeber === 'rolle'
557
+ ? tr('swap.guessedByRole', z.geratenRolle || '?')
558
+ : tr('swap.guessedByName', (z.altR || '').split('.').pop());
559
+ nm.appendChild(rm);
560
+ }
464
561
  r.appendChild(nm);
465
562
  var rechts = el('span');
563
+ /* Das Kaestchen „entfernen" - an zwei Stellen gebraucht, deshalb hier.
564
+
565
+ Es gab es bisher nur, wenn GAR NICHTS gefunden wurde. Der zweite
566
+ Fall braucht es genauso: Lesequelle gefunden, Schreibziel nicht.
567
+ Ein Punkt mit Schreibrecht ohne Schreibziel laesst sich in ioBroker
568
+ nicht ausdruecken - jede Form, die der js-controller annimmt,
569
+ schreibt irgendwohin, und `{read: X}` ohne `write` lehnt er ab
570
+ („The id is empty", gemessen 12.09.2026). Also entscheidet der
571
+ Nutzer: mitnehmen geht nicht, entfernen schon (Ricardo). */
572
+ var entfernenKaestchen = function () {
573
+ var lb = document.createElement('label');
574
+ lb.style.display = 'flex';
575
+ lb.style.gap = '6px';
576
+ lb.style.alignItems = 'center';
577
+ lb.style.marginTop = '3px';
578
+ lb.style.cursor = 'pointer';
579
+ var cb = document.createElement('input');
580
+ cb.type = 'checkbox';
581
+ cb.checked = !!S.tauschWeg[z.id];
582
+ cb.addEventListener('change', function () {
583
+ S.tauschWeg[z.id] = cb.checked;
584
+ /* Der Knopf haengt daran - also neu rechnen, nicht erst beim
585
+ naechsten Oeffnen. `zeichneTausch`, nicht `zeigeTausch`: das
586
+ zweite setzt Auswahl, Plan und Haken zurueck und faengt von
587
+ vorn an. */
588
+ zeichneTausch();
589
+ });
590
+ lb.appendChild(cb);
591
+ lb.appendChild(el('span', 'hint', tr('swap.removeIt')));
592
+ return lb;
593
+ };
466
594
  /* Beschriftet, sonst raet man, welche der beiden Zeilen die neue ist. */
467
595
  var paarZeile = function (beschriftung, text, klasse, farbe) {
468
596
  var zz = el('div');
@@ -489,6 +617,7 @@ function zeichneTausch() {
489
617
  kw.style.color = 'var(--bad)';
490
618
  kw.textContent = tr('swap.noWriteMatch');
491
619
  rechts.appendChild(kw);
620
+ rechts.appendChild(entfernenKaestchen());
492
621
  }
493
622
  } else {
494
623
  var fehlt = el('div');
@@ -497,19 +626,7 @@ function zeichneTausch() {
497
626
  rechts.appendChild(fehlt);
498
627
  /* Was nichts findet, laesst sich mit entfernen - sonst bliebe ein
499
628
  Punkt zurueck, der ins Leere zeigt (Ricardos Wahl, 25.08.2026). */
500
- var lb = document.createElement('label');
501
- lb.style.display = 'flex';
502
- lb.style.gap = '6px';
503
- lb.style.alignItems = 'center';
504
- lb.style.marginTop = '3px';
505
- lb.style.cursor = 'pointer';
506
- var cb = document.createElement('input');
507
- cb.type = 'checkbox';
508
- cb.checked = !!S.tauschWeg[z.id];
509
- cb.addEventListener('change', function () { S.tauschWeg[z.id] = cb.checked; });
510
- lb.appendChild(cb);
511
- lb.appendChild(el('span', 'hint', tr('swap.removeIt')));
512
- rechts.appendChild(lb);
629
+ rechts.appendChild(entfernenKaestchen());
513
630
  }
514
631
  r.appendChild(rechts);
515
632
  k.appendChild(r);
@@ -556,7 +673,39 @@ function zeichneTausch() {
556
673
  if (plan.some(function (z) { return z.stufe === 3; })) {
557
674
  body.appendChild(el('div', 'aside', tr('swap.guessHint')));
558
675
  }
559
- if (go) { go.disabled = false; }
676
+
677
+ /* Punkte, die nicht mitkoennen, halten den Tausch auf - bis der Nutzer
678
+ sagt, was mit ihnen geschehen soll.
679
+
680
+ Zwei Faelle, beide mit demselben Ausgang (das Kaestchen daneben):
681
+ gar kein Treffer, oder getrennte Quellen mit Lesetreffer und ohne
682
+ Schreibziel. Frueher lief der Tausch in beiden Faellen los. Der
683
+ zweite endete dabei im Fehler des js-controllers, und weil
684
+ `tauscheAus` Objekt fuer Objekt schrieb, stand der Alias danach
685
+ halb auf der alten und halb auf der neuen Quelle (W20, gemessen
686
+ 12.09.2026). Der erste lief still durch und liess den Punkt auf der
687
+ alten Quelle stehen - derselbe Mischzustand, nur ohne Meldung.
688
+
689
+ Gesperrt wird mit Begruendung am Knopf, nicht bloss grau: ein
690
+ Knopf, der nicht sagt warum, ist der Fehler aus I32. */
691
+ var offeneStolper = plan.filter(function (z) {
692
+ if (S.tauschWeg[z.id]) { return false; }
693
+ if (!z.neuR) { return true; }
694
+ return !!(z.altW && z.altW !== z.altR && !z.neuW);
695
+ });
696
+ if (go) {
697
+ if (offeneStolper.length) {
698
+ go.disabled = true;
699
+ go.title = tr('swap.blocked', offeneStolper.map(function (z) { return z.name; }).join(', '));
700
+ var sp = el('div', 'aside w');
701
+ sp.style.borderLeftColor = 'var(--bad)';
702
+ sp.appendChild(el('b', null, go.title));
703
+ body.appendChild(sp);
704
+ } else {
705
+ go.disabled = false;
706
+ go.title = '';
707
+ }
708
+ }
560
709
  }
561
710
 
562
711
  export function tauscheAus() {
@@ -585,6 +734,17 @@ export function tauscheAus() {
585
734
  };
586
735
 
587
736
  var weiter = function () {
737
+ /* Schlug beim Umschreiben etwas fehl, wird nichts geloescht.
738
+
739
+ Dieselbe Regel wie beim Verlegen (T11): erst alles Neue, und geht
740
+ dabei etwas schief, bleibt der Rest unberuehrt. Hier fehlte sie -
741
+ `weiter` loeschte auch dann, wenn die Haelfte der Punkte noch auf
742
+ der alten Quelle stand. Zurueck kann die Werkbank nichts nehmen,
743
+ aber sie kann aufhoeren, es schlimmer zu machen: der Nutzer sieht
744
+ die Fehler und drueckt „Noch einmal versuchen", und die schon
745
+ umgeschriebenen Punkte stoeren dabei nicht - sie stehen ja bereits
746
+ richtig (12.09.2026). */
747
+ if (fehler.length) { return fertig(); }
588
748
  if (!loeschen.length) { return fertig(); }
589
749
  var offen2 = loeschen.length;
590
750
  loeschen.forEach(function (id) {
@@ -445,12 +445,31 @@ export function zuSchreiben(alleAusgaenge) {
445
445
  }).filter(Boolean);
446
446
  }
447
447
  if (!alleAusgaenge || !S.entwurf.instanzen || S.entwurf.instanzen.length < 2) { return [S.entwurf]; }
448
+ /* Der Kanalname wird zur Kennung - und zwei koennen dabei gleich werden.
449
+
450
+ `ausgangName` saeubert seit dem 12.09.2026 selbst (G31), und damit
451
+ ergeben „Licht Bar" und „Licht, Bar" beide `Licht_Bar`: der zweite
452
+ Kanal ueberschriebe den ersten, still und ohne Meldung. Deshalb
453
+ wird hier mitgezaehlt, was schon vergeben ist - der zweite bekommt
454
+ `_2`, der dritte `_3`. Der Anzeigename bleibt der des Kanals. */
455
+ var vergeben = {};
456
+ var eindeutig = function (name) {
457
+ if (!vergeben[name]) { vergeben[name] = 1; return name; }
458
+ var z = 1, n2;
459
+ do { z++; n2 = name + '_' + z; } while (vergeben[n2]);
460
+ vergeben[n2] = 1;
461
+ return n2;
462
+ };
448
463
  return S.entwurf.instanzen.map(function (n) {
449
- if (String(n) === String(S.entwurf.instanz)) { return S.entwurf; }
464
+ if (String(n) === String(S.entwurf.instanz)) {
465
+ S.entwurf.zielName = eindeutig(S.entwurf.zielName);
466
+ S.entwurf.ziel = S.entwurf.zielOrdner + '.' + S.entwurf.zielName;
467
+ return S.entwurf;
468
+ }
450
469
  var v = vorschlag(S.current, S.entwurf.vorlage, n);
451
470
  if (v) {
452
471
  v.zielOrdner = S.entwurf.zielOrdner;
453
- v.zielName = ausgangName(v, n);
472
+ v.zielName = eindeutig(ausgangName(v, n));
454
473
  v.ziel = v.zielOrdner + '.' + v.zielName;
455
474
  enumsUebertragen(v);
456
475
  }
@@ -487,6 +506,35 @@ function sammle(liste, o) {
487
506
  vorh.neu = vorh.neu && o.neu;
488
507
  }
489
508
 
509
+ /* Eine selbst angelegte Aufzaehlung, die durch diesen Schreibvorgang
510
+ leer wird, geht mit.
511
+
512
+ Beim LOESCHEN eines Alias tut das `enumsDieLeerWuerden` seit jeher.
513
+ Beim blossen WECHSEL der Funktion nicht: wer „Stehlampe" waehlte,
514
+ schrieb, dann auf „Licht" umstellte, hinterliess ein leeres
515
+ `enum.functions.floor_lamp` - von der Werkbank angelegt, von der
516
+ Werkbank geleert, und trotzdem stehengeblieben (gemessen 12.09.2026).
517
+
518
+ Dieselbe Regel wie dort: nur was `angelegtVon` traegt, und nur wenn
519
+ hinterher kein Mitglied uebrig ist. Der Eintrag wandert aus der
520
+ Schreibliste in die Loeschliste - sonst schriebe ihn `setObject`
521
+ parallel zum `delObject` wieder hin.
522
+
523
+ Gerufen von `zeigeTrockenlauf` UND `schreibeObjekte`: beide bauen ihre
524
+ Liste selbst, und was der Trockenlauf zeigt, muss das Schreiben tun. */
525
+ function leereEigeneEnums(liste, weg) {
526
+ liste.slice().forEach(function (x) {
527
+ if (!x.istEnum || !x.weg) { return; }
528
+ var o = enums[x.id];
529
+ if (!o || !o.native || o.native.angelegtVon !== 'alias-workbench') { return; }
530
+ if (((x.obj.common || {}).members || []).length) { return; }
531
+ liste.splice(liste.indexOf(x), 1);
532
+ if (!weg.some(function (y) { return y.id === x.id; })) {
533
+ weg.push({ id: x.id, vorgabe: true, grund: tr('enums.nowEmpty') });
534
+ }
535
+ });
536
+ }
537
+
490
538
  export function zeigeTrockenlauf(alleAusgaenge) {
491
539
  if (!S.entwurf) { return; }
492
540
  S.trockenAlle = !!alleAusgaenge;
@@ -532,6 +580,7 @@ export function zeigeTrockenlauf(alleAusgaenge) {
532
580
  if (!weg.some(function (x) { return x.id === v.id; })) { weg.push(v); }
533
581
  });
534
582
  });
583
+ leereEigeneEnums(liste, weg);
535
584
  S.loeschListe = weg;
536
585
  var body = $('#dry-body');
537
586
  body.textContent = '';
@@ -600,6 +649,20 @@ export function zeigeTrockenlauf(alleAusgaenge) {
600
649
  body.appendChild(zwK);
601
650
  }
602
651
 
652
+ /* Was beim letzten Anlegen der Sendepunkte schiefging - hier oben, wo
653
+ man es sieht, und nicht als Tooltip an einem Knopf, den derselbe
654
+ Rueckruf gleich wieder wegwirft (H17). */
655
+ if (S.mqFehler && S.mqFehler.length) {
656
+ var mk = el('div', 'aside w');
657
+ mk.style.borderLeftColor = 'var(--bad)';
658
+ mk.style.marginBottom = '11px';
659
+ mk.appendChild(el('b', null, tr('write.failed', S.mqFehler.length)));
660
+ var mul = el('ul');
661
+ S.mqFehler.forEach(function (x) { mul.appendChild(el('li', null, x)); });
662
+ mk.appendChild(mul);
663
+ body.appendChild(mk);
664
+ }
665
+
603
666
  /* Die Aufzaehlungen sind gerade nicht bekannt — dann wird auch keine
604
667
  geschrieben. Das gehoert hierher und nicht in eine Sperre: der Alias
605
668
  selbst braucht sie nicht, und aus einem stillen Datenverlust eine
@@ -618,7 +681,12 @@ export function zeigeTrockenlauf(alleAusgaenge) {
618
681
  var wch = el('div', 'ch');
619
682
  wch.appendChild(el('span', 'typ', tr('write.remove')));
620
683
  wch.appendChild(el('span', 'chip bad', tr('write.removeCount', weg.length)));
621
- wch.appendChild(el('span', 'chip mut', tr('write.removeHint')));
684
+ /* Der Satz gilt fuer Datenpunkte. Steht eine leer gewordene
685
+ Aufzaehlung mit in der Liste, waere er dort falsch - die liegt
686
+ nicht im Alias und stand nie im Entwurf (12.09.2026). */
687
+ if (weg.some(function (v) { return String(v.id).indexOf('enum.') !== 0; })) {
688
+ wch.appendChild(el('span', 'chip mut', tr('write.removeHint')));
689
+ }
622
690
  wk.appendChild(wch);
623
691
  weg.forEach(function (v, i) {
624
692
  var r = el('div', 'slot');
@@ -685,6 +753,19 @@ export function zeigeTrockenlauf(alleAusgaenge) {
685
753
  kn2.disabled = false;
686
754
  kn2.textContent = tr('write.retry');
687
755
  kn2.title = mqFehler.join(' | ');
756
+ /* Und zwar so, dass es den Neuaufbau ueberlebt.
757
+
758
+ Drei Zeilen weiter unten wird der Entwurf neu gebaut
759
+ und der Trockenlauf noch einmal gerechnet - dabei
760
+ verschwindet dieser Knopf samt seiner Begruendung aus
761
+ dem DOM. Gemessen 12.09.2026: nach rund 120 ms war er
762
+ weg. Der neue Dialog sperrte wieder und sagte, welcher
763
+ Sendepunkt fehlt, aber nicht mehr, WARUM er nicht
764
+ entstanden ist (H17). Am Zustand gemerkt, zeichnet
765
+ `zeigeTrockenlauf` ihn oben wieder hin. */
766
+ S.mqFehler = mqFehler.slice();
767
+ } else {
768
+ S.mqFehler = null;
688
769
  }
689
770
  /* Erst den Entwurf neu aufbauen lassen, dann den
690
771
  Trockenlauf noch einmal rechnen. Sofort geoeffnet zeigte
@@ -1024,6 +1105,16 @@ export function loescheAlias() {
1024
1105
  m.style.fontSize = '13px';
1025
1106
  if (fehler.length) {
1026
1107
  m.appendChild(el('b', null, tr('write.failed', fehler.length)));
1108
+ /* Und WAS schiefging, nicht nur wie viel.
1109
+
1110
+ Beim Schreiben steht die Liste seit jeher da (`melden` beim
1111
+ Verlegen, der Trockenlauf-Dialog beim Anlegen); beim Loeschen
1112
+ fehlte sie, und zurueck blieb die blosse Zahl. Wer wissen
1113
+ will, welcher Punkt haengt — etwa weil ein Skript ihn sperrt
1114
+ —, erfuhr es nicht (G44, gemessen 12.09.2026). */
1115
+ var ul = el('ul');
1116
+ fehler.forEach(function (x) { ul.appendChild(el('li', null, x)); });
1117
+ m.appendChild(ul);
1027
1118
  } else {
1028
1119
  m.appendChild(el('b', null, tr('del.done', alle.length)));
1029
1120
  }
@@ -1674,6 +1765,10 @@ export function schreibeObjekte() {
1674
1765
  schlecht += pruefeSchreiben(en).length;
1675
1766
  });
1676
1767
  if (schlecht) { return; }
1768
+ /* Dieselbe Bereinigung wie im Trockenlauf - diese Liste entsteht neu,
1769
+ und ohne den Griff hier schriebe `setObject` die leere Aufzaehlung
1770
+ parallel zum `delObject` wieder hin. */
1771
+ leereEigeneEnums(liste, S.loeschListe);
1677
1772
 
1678
1773
  var body = $('#dry-body');
1679
1774
  var offen = liste.length;
@@ -11,7 +11,9 @@ import { $ } from './basis.js';
11
11
  export var expertenVorgabe = false;
12
12
 
13
13
  /* ================== Sprachen ==================
14
- Woerterbuch als flache Schluessel-Text-Karte unter admin/i18n/.
14
+ Woerterbuch als flache Schluessel-Text-Karte unter admin/sprachen/.
15
+ (Nicht admin/i18n/ - dort liegt das Woerterbuch der Einstellungsseite,
16
+ das der Admin selbst laedt.)
15
17
  Neue Sprache = neue Datei, sonst nichts. Faellt eine Uebersetzung
16
18
  aus, greift Englisch, danach der Schluessel selbst — so bleibt die
17
19
  Oberflaeche bedienbar statt leer. */
@@ -38,7 +40,7 @@ export function ladeSprache(fertig) {
38
40
  var vorgabe = (navigator.language || 'en').slice(0, 2).toLowerCase();
39
41
 
40
42
  function hole(code, ziel, danach) {
41
- fetch('./i18n/' + code + '.json')
43
+ fetch('./sprachen/' + code + '.json')
42
44
  .then(function (r) { return r.ok ? r.json() : {}; })
43
45
  .then(function (d) { Object.keys(d).forEach(function (k) { ziel[k] = d[k]; }); danach(); })
44
46
  .catch(function () { danach(); });
@@ -54,7 +56,12 @@ export function ladeSprache(fertig) {
54
56
 
55
57
  var beantwortet = false;
56
58
  socket.emit('getObject', 'system.config', function (err, obj) {
57
- if (beantwortet) { return; }
59
+ /* Hier stand `if (beantwortet) { return; }` - der Waechter aus der
60
+ Zeit vor dem Nachziehen. Er stieg genau im Fall aus, fuer den der
61
+ Zweig weiter unten gebaut wurde, und machte ihn zu totem Text
62
+ (gemessen 12.09.2026: mit `expertMode: true` am System und einer
63
+ um 4 s verzoegerten Antwort blieb `expertenVorgabe` auch nach
64
+ zehn Sekunden auf `false`). */
58
65
  /* Auch eine spaete Antwort gilt noch.
59
66
 
60
67
  Nach 2,5 Sekunden geht es ohne weiter — richtig, sonst haengt der
@@ -7,7 +7,7 @@
7
7
  import { S } from './zustand.js';
8
8
  import { D, $, el } from './basis.js';
9
9
  import { hatPunkt, jsonVon, feldWert, kindZustaende, feldFormel } from './werte.js';
10
- import { eigenerKanalName, merkeHakenVorgabe } from './entwurf.js';
10
+ import { eigenerKanalName, merkeHakenVorgabe, kennungtauglich } from './entwurf.js';
11
11
  import { tasmotaBefehle } from './mqtt.js';
12
12
  import { socket } from './verbindung.js';
13
13
 
@@ -271,7 +271,17 @@ export function setzeInstanz(text, v, n) {
271
271
  export function ausgangName(e, n) {
272
272
  if (e && e.kanal) {
273
273
  var eigen = eigenerKanalName(e.kanal + '.' + n);
274
- if (eigen) { return eigen.replace(/\./g, '_'); }
274
+ /* Der Kanalname wird hier zur KENNUNG - also durch dieselbe
275
+ Saeuberung wie ueberall sonst.
276
+
277
+ Vorher stand hier nur `replace(/\./g, '_')`. Leerzeichen und
278
+ Kommas blieben damit stehen, und der js-controller lehnt so etwas
279
+ nicht ab, er saeubert still: aus dem Kanalnamen „Licht, Bar" wurde
280
+ `alias.0.…Licht_ Bar`, waehrend die Werkbank „Licht, Bar" anzeigte
281
+ und in die Aufzaehlungen eintrug - eine Kennung, die es nie gab
282
+ (G31, gemessen 12.09.2026). Der Name selbst bleibt unberuehrt: er
283
+ steht als Beschriftung im Objekt. */
284
+ if (eigen) { return kennungtauglich(eigen) || eigen.replace(/\./g, '_'); }
275
285
  }
276
286
  if (!e || !e.kanalnameRoh) { return 'POWER' + n; }
277
287
  return String(e.kanalnameRoh).split(e.platzhalter || '%N%').join(n);