iobroker.alias-workbench 0.9.9 → 0.9.11

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) {
@@ -28,6 +28,7 @@ import { zeichneErgebnis } from './ergebnis.js';
28
28
  import { uebernehmeBestand , enumAenderungen, setzeZiel } from './zuordnung.js';
29
29
 
30
30
  import { mqttEinzelnStill } from './mqtt.js';
31
+ import { sucheInSkripten, sucheInSkriptenMehrere, skriptListe } from './verwendung.js';
31
32
 
32
33
  import { rateBehalten } from './vorschlagen.js';
33
34
 
@@ -270,20 +271,33 @@ export function baueObjekte(e) {
270
271
  } });
271
272
 
272
273
  e.states.forEach(function (s) {
273
- if (!s.on || !s.n || !s.srcR) { return; }
274
+ if (!s.on || !s.n) { return; }
275
+ /* Ohne Lesequelle faellt die Zeile heraus - ausser sie ist ein
276
+ Taster, der nur schreibt (K23). */
277
+ if (!s.srcR && !taster(s)) { return; }
274
278
  var id = ziel + '.' + s.n;
275
279
 
276
280
  var alias = {};
277
- if (s.srcW && s.srcW !== s.srcR) { alias.id = { read: s.srcR, write: s.srcW }; }
281
+ /* ioBroker verlangt immer eine `alias.id`; ein reines `{ write: … }`
282
+ weist der js-controller ab („Alias id is invalid: The id is
283
+ empty!", gemessen 15.09.2026). Beim Taster steht deshalb das
284
+ Schreibziel darin, und `common.read: false` sagt, dass dort nichts
285
+ abzuholen ist. */
286
+ if (!s.srcR) { alias.id = s.srcW; }
287
+ else if (s.srcW && s.srcW !== s.srcR) { alias.id = { read: s.srcR, write: s.srcW }; }
278
288
  else { alias.id = s.srcR; }
279
289
  if (s.f) { alias.read = s.f; }
280
290
  if (s.fw && s.srcW) { alias.write = s.fw; }
281
291
 
292
+ /* Ein Taster meldet nichts — dann sagt der Alias-Punkt das auch.
293
+ Sonst malen vis, Alexa und matter einen Kippschalter, der ewig auf
294
+ „aus" steht, statt eines Knopfes. */
295
+ var istTaster = taster(s);
282
296
  var common = {
283
297
  name: s.caption || s.n,
284
298
  role: s.role,
285
299
  type: s.typ || 'mixed',
286
- read: true,
300
+ read: !istTaster,
287
301
  write: !!s.srcW,
288
302
  alias: alias
289
303
  };
@@ -349,6 +363,19 @@ export function verwaiste(e) {
349
363
  }
350
364
 
351
365
  /* Was dem Schreiben im Weg steht. */
366
+ /* Ein Taster: keine Lesequelle, ein Schreibziel, und das Ziel sagt
367
+ selbst, dass es nichts zu melden hat (`common.read === false`).
368
+
369
+ Nicht ueber die Rolle entschieden: `button.stop` ist Konvention, aber
370
+ nur der Adapter weiss wirklich, ob am Punkt etwas abzulesen ist. An
371
+ `hm-rpc.2.NEQ1795694.1.STOP` steht `read: false` — hingeschrieben von
372
+ hm-rpc, nicht von uns. */
373
+ export function taster(s) {
374
+ if (!s || s.srcR || !s.srcW) { return false; }
375
+ var o = S.objects[s.srcW];
376
+ return !!(o && o.common && o.common.read === false);
377
+ }
378
+
352
379
  export function pruefeSchreiben(e) {
353
380
  var probleme = [];
354
381
  var an = e.states.filter(function (s) { return s.on; });
@@ -361,7 +388,20 @@ export function pruefeSchreiben(e) {
361
388
  namen[s.n] = 1;
362
389
 
363
390
  if (!s.srcR) {
364
- probleme.push(tr('write.noSourceChosen', s.n));
391
+ /* Ein Taster hat keinen Zustand zum Lesen.
392
+
393
+ `STOP` am Rollladen schreibt nur; am Geraet traegt der Punkt
394
+ `read: false`, der hm-rpc-Adapter setzt das selbst. Die Sperre
395
+ hier war deshalb richtig UND falsch zugleich: richtig, weil
396
+ ioBroker ohne `alias.id` keinen Punkt anlegt — falsch, weil sie
397
+ damit jeden Rollladen-Alias mit STOP unmoeglich machte. Kein
398
+ einziger ist je entstanden (gefunden 14.09.2026, K23).
399
+
400
+ Massgeblich ist, was das ZIEL kann, nicht was die Rolle
401
+ nahelegt: `read: false` ist eine Tatsache am Objekt, die der
402
+ Adapter hinschreibt. Beim Schreiben nimmt `alias.id` dann das
403
+ Schreibziel, und der Alias-Punkt bekommt selbst `read: false`. */
404
+ if (!(s.srcW && taster(s))) { probleme.push(tr('write.noSourceChosen', s.n)); }
365
405
  } else if (!S.objects[s.srcR]) {
366
406
  /* Der teuerste Fehler: der js-controller merkt sich „Quelle fehlt"
367
407
  dauerhaft und korrigiert das nie. Nur loeschen und neu anlegen
@@ -445,12 +485,31 @@ export function zuSchreiben(alleAusgaenge) {
445
485
  }).filter(Boolean);
446
486
  }
447
487
  if (!alleAusgaenge || !S.entwurf.instanzen || S.entwurf.instanzen.length < 2) { return [S.entwurf]; }
488
+ /* Der Kanalname wird zur Kennung - und zwei koennen dabei gleich werden.
489
+
490
+ `ausgangName` saeubert seit dem 12.09.2026 selbst (G31), und damit
491
+ ergeben „Licht Bar" und „Licht, Bar" beide `Licht_Bar`: der zweite
492
+ Kanal ueberschriebe den ersten, still und ohne Meldung. Deshalb
493
+ wird hier mitgezaehlt, was schon vergeben ist - der zweite bekommt
494
+ `_2`, der dritte `_3`. Der Anzeigename bleibt der des Kanals. */
495
+ var vergeben = {};
496
+ var eindeutig = function (name) {
497
+ if (!vergeben[name]) { vergeben[name] = 1; return name; }
498
+ var z = 1, n2;
499
+ do { z++; n2 = name + '_' + z; } while (vergeben[n2]);
500
+ vergeben[n2] = 1;
501
+ return n2;
502
+ };
448
503
  return S.entwurf.instanzen.map(function (n) {
449
- if (String(n) === String(S.entwurf.instanz)) { return S.entwurf; }
504
+ if (String(n) === String(S.entwurf.instanz)) {
505
+ S.entwurf.zielName = eindeutig(S.entwurf.zielName);
506
+ S.entwurf.ziel = S.entwurf.zielOrdner + '.' + S.entwurf.zielName;
507
+ return S.entwurf;
508
+ }
450
509
  var v = vorschlag(S.current, S.entwurf.vorlage, n);
451
510
  if (v) {
452
511
  v.zielOrdner = S.entwurf.zielOrdner;
453
- v.zielName = ausgangName(v, n);
512
+ v.zielName = eindeutig(ausgangName(v, n));
454
513
  v.ziel = v.zielOrdner + '.' + v.zielName;
455
514
  enumsUebertragen(v);
456
515
  }
@@ -487,6 +546,35 @@ function sammle(liste, o) {
487
546
  vorh.neu = vorh.neu && o.neu;
488
547
  }
489
548
 
549
+ /* Eine selbst angelegte Aufzaehlung, die durch diesen Schreibvorgang
550
+ leer wird, geht mit.
551
+
552
+ Beim LOESCHEN eines Alias tut das `enumsDieLeerWuerden` seit jeher.
553
+ Beim blossen WECHSEL der Funktion nicht: wer „Stehlampe" waehlte,
554
+ schrieb, dann auf „Licht" umstellte, hinterliess ein leeres
555
+ `enum.functions.floor_lamp` - von der Werkbank angelegt, von der
556
+ Werkbank geleert, und trotzdem stehengeblieben (gemessen 12.09.2026).
557
+
558
+ Dieselbe Regel wie dort: nur was `angelegtVon` traegt, und nur wenn
559
+ hinterher kein Mitglied uebrig ist. Der Eintrag wandert aus der
560
+ Schreibliste in die Loeschliste - sonst schriebe ihn `setObject`
561
+ parallel zum `delObject` wieder hin.
562
+
563
+ Gerufen von `zeigeTrockenlauf` UND `schreibeObjekte`: beide bauen ihre
564
+ Liste selbst, und was der Trockenlauf zeigt, muss das Schreiben tun. */
565
+ function leereEigeneEnums(liste, weg) {
566
+ liste.slice().forEach(function (x) {
567
+ if (!x.istEnum || !x.weg) { return; }
568
+ var o = enums[x.id];
569
+ if (!o || !o.native || o.native.angelegtVon !== 'alias-workbench') { return; }
570
+ if (((x.obj.common || {}).members || []).length) { return; }
571
+ liste.splice(liste.indexOf(x), 1);
572
+ if (!weg.some(function (y) { return y.id === x.id; })) {
573
+ weg.push({ id: x.id, vorgabe: true, grund: tr('enums.nowEmpty') });
574
+ }
575
+ });
576
+ }
577
+
490
578
  export function zeigeTrockenlauf(alleAusgaenge) {
491
579
  if (!S.entwurf) { return; }
492
580
  S.trockenAlle = !!alleAusgaenge;
@@ -532,6 +620,7 @@ export function zeigeTrockenlauf(alleAusgaenge) {
532
620
  if (!weg.some(function (x) { return x.id === v.id; })) { weg.push(v); }
533
621
  });
534
622
  });
623
+ leereEigeneEnums(liste, weg);
535
624
  S.loeschListe = weg;
536
625
  var body = $('#dry-body');
537
626
  body.textContent = '';
@@ -600,6 +689,20 @@ export function zeigeTrockenlauf(alleAusgaenge) {
600
689
  body.appendChild(zwK);
601
690
  }
602
691
 
692
+ /* Was beim letzten Anlegen der Sendepunkte schiefging - hier oben, wo
693
+ man es sieht, und nicht als Tooltip an einem Knopf, den derselbe
694
+ Rueckruf gleich wieder wegwirft (H17). */
695
+ if (S.mqFehler && S.mqFehler.length) {
696
+ var mk = el('div', 'aside w');
697
+ mk.style.borderLeftColor = 'var(--bad)';
698
+ mk.style.marginBottom = '11px';
699
+ mk.appendChild(el('b', null, tr('write.failed', S.mqFehler.length)));
700
+ var mul = el('ul');
701
+ S.mqFehler.forEach(function (x) { mul.appendChild(el('li', null, x)); });
702
+ mk.appendChild(mul);
703
+ body.appendChild(mk);
704
+ }
705
+
603
706
  /* Die Aufzaehlungen sind gerade nicht bekannt — dann wird auch keine
604
707
  geschrieben. Das gehoert hierher und nicht in eine Sperre: der Alias
605
708
  selbst braucht sie nicht, und aus einem stillen Datenverlust eine
@@ -618,7 +721,12 @@ export function zeigeTrockenlauf(alleAusgaenge) {
618
721
  var wch = el('div', 'ch');
619
722
  wch.appendChild(el('span', 'typ', tr('write.remove')));
620
723
  wch.appendChild(el('span', 'chip bad', tr('write.removeCount', weg.length)));
621
- wch.appendChild(el('span', 'chip mut', tr('write.removeHint')));
724
+ /* Der Satz gilt fuer Datenpunkte. Steht eine leer gewordene
725
+ Aufzaehlung mit in der Liste, waere er dort falsch - die liegt
726
+ nicht im Alias und stand nie im Entwurf (12.09.2026). */
727
+ if (weg.some(function (v) { return String(v.id).indexOf('enum.') !== 0; })) {
728
+ wch.appendChild(el('span', 'chip mut', tr('write.removeHint')));
729
+ }
622
730
  wk.appendChild(wch);
623
731
  weg.forEach(function (v, i) {
624
732
  var r = el('div', 'slot');
@@ -685,6 +793,19 @@ export function zeigeTrockenlauf(alleAusgaenge) {
685
793
  kn2.disabled = false;
686
794
  kn2.textContent = tr('write.retry');
687
795
  kn2.title = mqFehler.join(' | ');
796
+ /* Und zwar so, dass es den Neuaufbau ueberlebt.
797
+
798
+ Drei Zeilen weiter unten wird der Entwurf neu gebaut
799
+ und der Trockenlauf noch einmal gerechnet - dabei
800
+ verschwindet dieser Knopf samt seiner Begruendung aus
801
+ dem DOM. Gemessen 12.09.2026: nach rund 120 ms war er
802
+ weg. Der neue Dialog sperrte wieder und sagte, welcher
803
+ Sendepunkt fehlt, aber nicht mehr, WARUM er nicht
804
+ entstanden ist (H17). Am Zustand gemerkt, zeichnet
805
+ `zeigeTrockenlauf` ihn oben wieder hin. */
806
+ S.mqFehler = mqFehler.slice();
807
+ } else {
808
+ S.mqFehler = null;
688
809
  }
689
810
  /* Erst den Entwurf neu aufbauen lassen, dann den
690
811
  Trockenlauf noch einmal rechnen. Sofort geoeffnet zeigte
@@ -988,6 +1109,32 @@ export function zeichneLoeschen() {
988
1109
  body.appendChild(ew);
989
1110
  }
990
1111
 
1112
+ /* Wer nennt diese Kennungen sonst noch?
1113
+
1114
+ Beim Verlegen steht die Liste seit jeher da, beim Loeschen fehlte
1115
+ sie - dabei ist die Folge hier endgueltig: ein Skript, das den
1116
+ Alias anspricht, laeuft danach ins Leere. Gesucht wird nur im
1117
+ Klartext und nur in den Skripten; der Hinweis darunter sagt das. */
1118
+ var skriptFeld = el('div');
1119
+ skriptFeld.style.marginTop = '11px';
1120
+ body.appendChild(skriptFeld);
1121
+ var wurzeln = [ziel].concat(S.loeschAlleAusgaenge ? geschwisterVon(ziel) : []);
1122
+ sucheInSkriptenMehrere(wurzeln, function (treffer) {
1123
+ if (S.loeschZiel !== ziel) { return; }
1124
+ skriptFeld.textContent = '';
1125
+ if (!treffer.length) {
1126
+ skriptFeld.appendChild(el('div', 'aside', tr('move.noScripts')));
1127
+ return;
1128
+ }
1129
+ var w = el('div', 'aside w');
1130
+ w.appendChild(el('b', null, treffer.length === 1
1131
+ ? tr('del.scriptsHead1')
1132
+ : tr('del.scriptsHead', treffer.length)));
1133
+ w.appendChild(skriptListe(treffer));
1134
+ w.appendChild(el('div', 'hint', tr('del.scriptsHint')));
1135
+ skriptFeld.appendChild(w);
1136
+ });
1137
+
991
1138
  var b = $('#btn-del-go');
992
1139
  b.disabled = false;
993
1140
  b.hidden = false;
@@ -1024,6 +1171,16 @@ export function loescheAlias() {
1024
1171
  m.style.fontSize = '13px';
1025
1172
  if (fehler.length) {
1026
1173
  m.appendChild(el('b', null, tr('write.failed', fehler.length)));
1174
+ /* Und WAS schiefging, nicht nur wie viel.
1175
+
1176
+ Beim Schreiben steht die Liste seit jeher da (`melden` beim
1177
+ Verlegen, der Trockenlauf-Dialog beim Anlegen); beim Loeschen
1178
+ fehlte sie, und zurueck blieb die blosse Zahl. Wer wissen
1179
+ will, welcher Punkt haengt — etwa weil ein Skript ihn sperrt
1180
+ —, erfuhr es nicht (G44, gemessen 12.09.2026). */
1181
+ var ul = el('ul');
1182
+ fehler.forEach(function (x) { ul.appendChild(el('li', null, x)); });
1183
+ m.appendChild(ul);
1027
1184
  } else {
1028
1185
  m.appendChild(el('b', null, tr('del.done', alle.length)));
1029
1186
  }
@@ -1135,41 +1292,6 @@ function verlegeUmfang(alt) {
1135
1292
  });
1136
1293
  }
1137
1294
 
1138
- /* Wer nennt diese Kennung sonst noch?
1139
-
1140
- Die Skripte des javascript-Adapters lassen sich durchsuchen; sie sind
1141
- der haeufigste Ort, an dem eine Kennung im Klartext steht. vis-
1142
- Ansichten und fremde Adapter kann die Werkbank nicht durchsehen -
1143
- darauf weist der Dialog eigens hin, statt Vollstaendigkeit
1144
- vorzutaeuschen. */
1145
- export function sucheInSkripten(kennung, fertig) {
1146
- var treffer = [];
1147
- var fertigMit = function () { fertig(treffer); };
1148
- try {
1149
- socket.emit('getObjectView', 'script', 'javascript',
1150
- { startkey: 'script.js.', endkey: 'script.js.\u9999' }, function (err, doc) {
1151
- if (err || !doc || !doc.rows) { return fertigMit(); }
1152
- doc.rows.forEach(function (r) {
1153
- var o = r.value;
1154
- var q = o && o.common && o.common.source;
1155
- if (!q || q.indexOf(kennung) === -1) { return; }
1156
- var zeilen = String(q).split('\n');
1157
- var stellen = [];
1158
- zeilen.forEach(function (z, i) {
1159
- if (z.indexOf(kennung) > -1) { stellen.push(i + 1); }
1160
- });
1161
- treffer.push({
1162
- id: r.id,
1163
- name: txt((o.common || {}).name) || r.id.split('.').pop(),
1164
- an: !!(o.common && o.common.enabled),
1165
- zeilen: stellen
1166
- });
1167
- });
1168
- fertigMit();
1169
- });
1170
- } catch { fertigMit(); }
1171
- }
1172
-
1173
1295
  export function zeigeVerlegen(altVorgabe, zielVorgabe) {
1174
1296
  /* Der Knopf steht auch an der Quelle, sobald es den Alias gibt (T1).
1175
1297
  Dort ist S.current die Quelle, nicht der Alias - gemeint ist immer
@@ -1440,14 +1562,7 @@ export function zeigeVerlegen(altVorgabe, zielVorgabe) {
1440
1562
  w3.appendChild(el('b', null, treffer.length === 1
1441
1563
  ? tr('move.scriptsHead1')
1442
1564
  : tr('move.scriptsHead', treffer.length)));
1443
- var ul3 = el('ul');
1444
- treffer.forEach(function (t) {
1445
- ul3.appendChild(el('li', null, t.name + (t.an ? '' : ' (' + tr('move.scriptOff') + ')') +
1446
- ' \u2014 ' + (t.zeilen.length === 1
1447
- ? tr('move.scriptLines', t.zeilen[0])
1448
- : tr('move.scriptLinesN', t.zeilen.join(', ')))));
1449
- });
1450
- w3.appendChild(ul3);
1565
+ w3.appendChild(skriptListe(treffer));
1451
1566
  skriptFeld.appendChild(w3);
1452
1567
  });
1453
1568
  };
@@ -1674,6 +1789,10 @@ export function schreibeObjekte() {
1674
1789
  schlecht += pruefeSchreiben(en).length;
1675
1790
  });
1676
1791
  if (schlecht) { return; }
1792
+ /* Dieselbe Bereinigung wie im Trockenlauf - diese Liste entsteht neu,
1793
+ und ohne den Griff hier schriebe `setObject` die leere Aufzaehlung
1794
+ parallel zum `delObject` wieder hin. */
1795
+ leereEigeneEnums(liste, S.loeschListe);
1677
1796
 
1678
1797
  var body = $('#dry-body');
1679
1798
  var offen = liste.length;
@@ -1754,9 +1873,32 @@ export function schreibeObjekte() {
1754
1873
  stehen, bis man den Reiter neu lud, und behauptete damit eine
1755
1874
  Abweichung, die es nicht mehr gibt. Nur bei Erfolg loeschen: ging
1756
1875
  etwas schief, ist die Aenderung tatsaechlich noch nicht drin. */
1757
- if (!fehler.length && S.entwurf && S.entwurf.states) {
1758
- S.entwurf.states.forEach(function (s) { s.geaendert = false; rateBehalten(s); });
1876
+ if (!fehler.length && S.entwurf) {
1877
+ if (S.entwurf.states) {
1878
+ S.entwurf.states.forEach(function (s) { s.geaendert = false; rateBehalten(s); });
1879
+ }
1759
1880
  S.entwurf.rateMeldung = null;
1881
+ /* Und dasselbe eine Ebene hoeher (G51, Ricardo, 15.09.2026).
1882
+
1883
+ `angefasst` sagt „hier steht etwas Ungeschriebenes". Die Marke
1884
+ wurde im ganzen Programm nur gesetzt, nie geloescht - weg war
1885
+ sie allein, wenn der Entwurf ganz wegfiel (Loeschen, Verlegen).
1886
+ Nach einem gelungenen Schreibvorgang blieb sie also stehen, und
1887
+ der naechste Klick im Baum fragte „Ungespeicherte Aenderungen …
1888
+ Wechselst du jetzt, sind sie weg" - ueber einen Alias, der
1889
+ gerade eben geschrieben worden war.
1890
+
1891
+ Die Marke faellt nur bei Erfolg. Ging etwas schief, ist die
1892
+ Aenderung tatsaechlich noch nicht drin, und die Frage ist dann
1893
+ berechtigt.
1894
+
1895
+ Sie hat eine zweite Aufgabe, und die erledigt sich mit: Solange
1896
+ `angefasst` steht, baut `neuZeichnenOderAufbauen` den Entwurf
1897
+ absichtlich NICHT neu auf - es soll ja nichts Ungeschriebenes
1898
+ ueberbuegeln. Das `holeZweig` unten laeuft gleich danach; jetzt
1899
+ baut es den Entwurf am frisch geschriebenen Alias neu auf,
1900
+ statt den alten stehenzulassen. */
1901
+ S.entwurf.angefasst = false;
1760
1902
  }
1761
1903
 
1762
1904
  /* Nur der Aliaszweig, nicht die ganze Datenbank. */
@@ -56,7 +56,12 @@ export function ladeSprache(fertig) {
56
56
 
57
57
  var beantwortet = false;
58
58
  socket.emit('getObject', 'system.config', function (err, obj) {
59
- 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`). */
60
65
  /* Auch eine spaete Antwort gilt noch.
61
66
 
62
67
  Nach 2,5 Sekunden geht es ohne weiter — richtig, sonst haengt der