iobroker.alias-workbench 0.9.0 → 0.9.8

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.
@@ -6,7 +6,7 @@ import { S } from './zustand.js';
6
6
  import { socket } from './verbindung.js';
7
7
  import { $, el } from './basis.js';
8
8
  import { tr, txt } from './sprache.js';
9
- import { enums, ladeEnums } from './enums.js';
9
+ import { enums, ladeEnums, enumsUnbekannt } from './enums.js';
10
10
  import { enumsVon } from './aufzaehlungen.js';
11
11
  import './katalog.js';
12
12
  import { holeZweig, indexNeu , nachziehenErledigt } from './objekte.js';
@@ -20,7 +20,8 @@ import {
20
20
  kennungtauglich,
21
21
  entwurfFuer,
22
22
  aliasFuer,
23
- knotenDa
23
+ knotenDa,
24
+ aboLoesen
24
25
  } from './entwurf.js';
25
26
  import { zeichneErgebnis } from './ergebnis.js';
26
27
 
@@ -221,7 +222,19 @@ export function baueObjekte(e) {
221
222
  der Versionen haette nichts mehr zu greifen. */
222
223
  var altN = (S.objects[ziel] && S.objects[ziel].native) || {};
223
224
  var ausAlias = (e.kanal.indexOf('alias.') === 0);
224
- var kanalNative = ausAlias ? JSON.parse(JSON.stringify(altN)) : { quelle: e.kanal };
225
+ /* Wie bei der Rolle eine Zeile weiter unten: mit dem alten Stand
226
+ beginnen und nur ueberschreiben, was wirklich beisteuert wird.
227
+
228
+ Vorher wurde `native` aus der Quellenansicht vollstaendig durch
229
+ `{ quelle }` ersetzt; Vorlage, Version und Geraetetyp kamen nur
230
+ zurueck, wenn gerade eine Vorlage griff. Ist sie geloescht oder
231
+ umbenannt, liefert `vorschlag` `null` — „Alias aktualisieren" bleibt
232
+ aber freigeschaltet, und die Herkunft war danach weg: kein Abzeichen
233
+ „gemerkt", womoeglich eine andere Vorlage bei der naechsten
234
+ Erkennung, und nichts mehr, woran ein Nachziehen der Versionen
235
+ greifen koennte. Gemessen 09.09.2026: aus fuenf Feldern wurde eins. */
236
+ var kanalNative = JSON.parse(JSON.stringify(altN));
237
+ if (!ausAlias) { kanalNative.quelle = e.kanal; }
225
238
  if (e.vorlage) {
226
239
  kanalNative.vorlage = e.vorlage;
227
240
  kanalNative.vorlageVersion = e.vorlageVersion;
@@ -378,6 +391,36 @@ export function zuSchreiben(alleAusgaenge) {
378
391
  Entwurf, benannt nach dem Kanal — nicht nach seiner Nummer. Aus
379
392
  „Kanal 1" wird `Licht_Bar`, weil das der Name ist, den jemand ihm
380
393
  gegeben hat. */
394
+ /* Raum und Funktion gehoeren an jeden Kanal, nicht nur an den
395
+ gezeigten.
396
+
397
+ Die Teil-Entwuerfe entstehen frisch aus `entwurfFuer` bzw.
398
+ `vorschlag` und trugen keins von beidem. `enumAenderungen` baute
399
+ deshalb fuer keinen einzigen Kanal ein Enum-Objekt: Wer oben einen
400
+ Raum einstellte und „Alle erzeugen" drueckte, bekam Kanaele ohne
401
+ Raum — der Einzelweg setzte ihn, der Sammelweg nicht.
402
+
403
+ Schlimmer war der zweite Fall: Fuer einen fremden Ausgang galt
404
+ `gewaehlt === ''`. Stand dessen Alias schon in einem Raum, fand ihn
405
+ `enumsVon`, und das gebaute Objekt trug ihn dort **aus**. Aus „ich
406
+ lege die anderen Ausgaenge auch an" wurde „ich nehme ihnen ihren
407
+ Raum weg" (gemessen 09.09.2026 an der Steckdosenleiste: Ausgang 2
408
+ verlor `enum.rooms.WBG30`).
409
+
410
+ Was ein Kanal schon hat, behaelt er — dieselbe Regel wie ueberall
411
+ sonst: der Bestand schlaegt den Vorschlag. Nur wer keine Zuordnung
412
+ hat, bekommt die eingestellte. */
413
+ var enumsUebertragen = function (v) {
414
+ if (!v || !v.ziel) { return v; }
415
+ [['rooms', 'raum', 'raumHer'], ['functions', 'funktion', 'funktionHer']]
416
+ .forEach(function (a) {
417
+ var eigen = enumsVon(v.ziel, a[0])[0] || '';
418
+ v[a[1]] = eigen || S.entwurf[a[1]] || '';
419
+ v[a[2]] = eigen ? 'bestand' : (v[a[1]] ? 'hand' : '');
420
+ });
421
+ return v;
422
+ };
423
+
381
424
  if (alleAusgaenge && S.entwurf.kanalGeraete && S.entwurf.kanalGeraete.length > 1) {
382
425
  /* Ein Ordner fuer das Geraet, darunter je Kanal ein Kanal — dieselbe
383
426
  Form, die die Tasmota-Mehrfachvorlage seit jeher erzeugt. */
@@ -387,10 +430,18 @@ export function zuSchreiben(alleAusgaenge) {
387
430
  var v = entwurfFuer(k.id, gem, S.entwurf.gemeinsamMitPlatz);
388
431
  if (!v) { return null; }
389
432
  v.zielOrdner = ordner;
390
- v.zielName = k.name;
391
- v.ziel = ordner + '.' + k.name;
433
+ /* Die Kennung, nicht der Anzeigename. „Licht Bar, oben" ergab eine
434
+ Kennung mit Leerzeichen und Komma, „Kueche.Decke" sogar eine
435
+ zusaetzliche Ebene samt Zwischenordner. Und der js-controller
436
+ lehnt so etwas nicht ab, er saeubert still: aus
437
+ `alias.0.wb[pruef]` wurde `alias.0.wb_pruef_`, der Rueckruf
438
+ meldete `null`. Das Objekt entstuende also unter einer anderen
439
+ Kennung als angezeigt (gemessen 08.09.2026). Der Name bleibt
440
+ davon unberuehrt — er steht als Beschriftung im Objekt. */
441
+ v.zielName = k.kennung || k.name;
442
+ v.ziel = ordner + '.' + v.zielName;
392
443
  uebernehmeBestand(v);
393
- return v;
444
+ return enumsUebertragen(v);
394
445
  }).filter(Boolean);
395
446
  }
396
447
  if (!alleAusgaenge || !S.entwurf.instanzen || S.entwurf.instanzen.length < 2) { return [S.entwurf]; }
@@ -401,11 +452,41 @@ export function zuSchreiben(alleAusgaenge) {
401
452
  v.zielOrdner = S.entwurf.zielOrdner;
402
453
  v.zielName = ausgangName(v, n);
403
454
  v.ziel = v.zielOrdner + '.' + v.zielName;
455
+ enumsUebertragen(v);
404
456
  }
405
457
  return v;
406
458
  }).filter(Boolean);
407
459
  }
408
460
 
461
+ /* Ein Objekt in die Liste, ohne ein vorhandenes zu verlieren.
462
+
463
+ Bei „Alle erzeugen" bauen mehrere Teil-Entwuerfe dasselbe Enum-Objekt,
464
+ jeder mit seiner eigenen Kennung darin. Die frueher hier stehende
465
+ Zeile `if (!liste.some(x => x.id === o.id)) liste.push(o)` warf das
466
+ zweite weg - von vier Ausgaengen landete einer im Raum, die anderen
467
+ drei fielen still unter den Tisch.
468
+
469
+ Gewoehnliche Objekte bleiben beim ersten: dort ist die Kennung
470
+ eindeutig, ein zweites waere derselbe Kanal. Nur Aufzaehlungen werden
471
+ vereinigt, und dabei gilt: eingetragen schlaegt ausgetragen. Ein
472
+ Mitglied, das ein Entwurf haben will, nimmt ihm kein anderer weg. */
473
+ function sammle(liste, o) {
474
+ var vorh = null;
475
+ liste.forEach(function (x) { if (!vorh && x.id === o.id) { vorh = x; } });
476
+ if (!vorh) { liste.push(o); return; }
477
+ if (!o.istEnum || !vorh.istEnum) { return; }
478
+ var m = ((vorh.obj.common || {}).members || []).slice();
479
+ ((o.obj.common || {}).members || []).forEach(function (x) {
480
+ if (m.indexOf(x) === -1) { m.push(x); }
481
+ });
482
+ m.sort();
483
+ vorh.obj.common.members = m;
484
+ vorh.dazu = vorh.dazu || o.dazu;
485
+ vorh.weg = vorh.weg && o.weg;
486
+ vorh.ikonDazu = vorh.ikonDazu || o.ikonDazu;
487
+ vorh.neu = vorh.neu && o.neu;
488
+ }
489
+
409
490
  export function zeigeTrockenlauf(alleAusgaenge) {
410
491
  if (!S.entwurf) { return; }
411
492
  S.trockenAlle = !!alleAusgaenge;
@@ -420,9 +501,7 @@ export function zeigeTrockenlauf(alleAusgaenge) {
420
501
  var liste = [];
421
502
  var probleme = [];
422
503
  entwuerfe.forEach(function (en) {
423
- baueObjekte(en).forEach(function (o) {
424
- if (!liste.some(function (x) { return x.id === o.id; })) { liste.push(o); }
425
- });
504
+ baueObjekte(en).forEach(function (o) { sammle(liste, o); });
426
505
  pruefeSchreiben(en).forEach(function (pr) { probleme.push(pr); });
427
506
  });
428
507
 
@@ -521,6 +600,17 @@ export function zeigeTrockenlauf(alleAusgaenge) {
521
600
  body.appendChild(zwK);
522
601
  }
523
602
 
603
+ /* Die Aufzaehlungen sind gerade nicht bekannt — dann wird auch keine
604
+ geschrieben. Das gehoert hierher und nicht in eine Sperre: der Alias
605
+ selbst braucht sie nicht, und aus einem stillen Datenverlust eine
606
+ stille Blockade zu machen waere kein Fortschritt (P8). */
607
+ if (enumsUnbekannt() && S.entwurf && (S.entwurf.raum || S.entwurf.funktion)) {
608
+ var ek = el('div', 'aside w');
609
+ ek.style.marginBottom = '11px';
610
+ ek.appendChild(el('b', null, tr('write.enumsUnknown')));
611
+ body.appendChild(ek);
612
+ }
613
+
524
614
  if (weg.length) {
525
615
  var wk = el('div', 'card');
526
616
  wk.style.marginBottom = '11px';
@@ -582,10 +672,20 @@ export function zeigeTrockenlauf(alleAusgaenge) {
582
672
  kn2.disabled = true;
583
673
  kn2.textContent = tr('write.writing');
584
674
  var offen2 = fehlend.length;
675
+ var mqFehler = [];
585
676
  fehlend.forEach(function (fid) {
586
677
  var t2 = fid.split('.cmnd.');
587
- mqttEinzelnStill(t2[0], t2[1], function () {
678
+ mqttEinzelnStill(t2[0], t2[1], function (err) {
679
+ if (err) { mqFehler.push(fid + ': ' + err); }
588
680
  if (--offen2 === 0) {
681
+ /* Was nicht entstanden ist, wird benannt — nicht gezaehlt.
682
+ Der Trockenlauf sperrt danach ohnehin wieder, aber ohne
683
+ diesen Satz suchte man den Grund am falschen Ende. */
684
+ if (mqFehler.length) {
685
+ kn2.disabled = false;
686
+ kn2.textContent = tr('write.retry');
687
+ kn2.title = mqFehler.join(' | ');
688
+ }
589
689
  /* Erst den Entwurf neu aufbauen lassen, dann den
590
690
  Trockenlauf noch einmal rechnen. Sofort geoeffnet zeigte
591
691
  er den Rohentwurf, weil die Vorlage noch nicht wieder
@@ -811,6 +911,30 @@ export function zeichneLoeschen() {
811
911
  hin.textContent = tr('del.hint');
812
912
  body.appendChild(hin);
813
913
 
914
+ /* Liegen eigene Kanaele darunter?
915
+
916
+ Der Umfang nimmt alles unterhalb des Praefixes. Bei einem
917
+ `device`-Container sind das auch fremde Alias-Kanaele — ganze
918
+ Geraete, die mit dem gemeinten nichts zu tun haben. Die Liste zeigt
919
+ sie zwar mit Typ, aber der Hinweistext sprach nur von „Datenpunkten
920
+ darunter", und die Zahl oben sagt nur, wie viele Zeilen es sind.
921
+ Wer sie zaehlt, weiss nicht, dass er drei Geraete mitnimmt. */
922
+ var kanaeleDrunter = alle.filter(function (id) {
923
+ if (id === ziel) { return false; }
924
+ var o = S.objects[id];
925
+ return o && (o.type === 'channel' || o.type === 'device');
926
+ });
927
+ if (kanaeleDrunter.length) {
928
+ var kw = el('div', 'aside w');
929
+ kw.style.marginBottom = '11px';
930
+ kw.appendChild(el('b', null, tr('del.channelsBelow', kanaeleDrunter.length)));
931
+ var kl = el('div', 'hint');
932
+ kl.style.marginTop = '4px';
933
+ kl.textContent = kanaeleDrunter.map(function (x) { return x.slice(ziel.length + 1); }).join(', ');
934
+ kw.appendChild(kl);
935
+ body.appendChild(kw);
936
+ }
937
+
814
938
  var gesch = geschwisterVon(ziel);
815
939
  if (gesch.length) {
816
940
  var gw = el('div', 'aside');
@@ -888,10 +1012,11 @@ export function loescheAlias() {
888
1012
  b.disabled = true;
889
1013
  b.textContent = tr('write.writing');
890
1014
 
891
- var offen = alle.length, fehler = [];
1015
+ var offen = alle.length, fehler = [], wirklichWeg = [];
892
1016
  alle.forEach(function (id) {
893
1017
  socket.emit('delObject', id, function (err) {
894
1018
  if (err) { fehler.push(id + ': ' + err); }
1019
+ else { wirklichWeg.push(id); }
895
1020
  if (--offen === 0) {
896
1021
  var body = $('#del-body');
897
1022
  body.textContent = '';
@@ -903,8 +1028,23 @@ export function loescheAlias() {
903
1028
  m.appendChild(el('b', null, tr('del.done', alle.length)));
904
1029
  }
905
1030
  body.appendChild(m);
906
- alle.forEach(function (x) { delete S.objects[x]; });
907
- S.keysSorted = Object.keys(S.objects).sort();
1031
+ /* Nur austragen, was wirklich weg ist.
1032
+
1033
+ Vorher fielen alle Kennungen aus dem eigenen Bestand, egal ob
1034
+ das Loeschen geklappt hatte. Was in der Datenbank stehenblieb,
1035
+ war danach ueber die Werkbank nicht mehr zu greifen - sie hielt
1036
+ es fuer geloescht (gemessen 09.09.2026: `…Neu.WORKING` stand am
1037
+ System, fehlte aber im Vorrat). */
1038
+ wirklichWeg.forEach(function (x) { delete S.objects[x]; });
1039
+ /* `indexNeu()` statt nur zu sortieren.
1040
+
1041
+ An vier Stellen stand hier `S.keysSorted = Object.keys(...)`,
1042
+ obwohl `indexNeu` importiert ist. Das raeumt zusaetzlich
1043
+ `S.kleinIndex` und zieht die Objektzahl in der Kopfzeile nach.
1044
+ Ohne das behielt die Karte geloeschte Kennungen — `hatPunkt`
1045
+ lieferte Treffer auf Punkte, die es nicht mehr gab —, und die
1046
+ Zahl oben blieb stehen (gefunden 09.09.2026). */
1047
+ indexNeu();
908
1048
 
909
1049
  /* Ordner, die dadurch leer geworden sind, mitnehmen — aber nur
910
1050
  die eigenen Zwischenebenen unterhalb von alias.0, und nur
@@ -920,11 +1060,22 @@ export function loescheAlias() {
920
1060
  } else {
921
1061
  b.hidden = true;
922
1062
  }
1063
+ /* Nach einem Teilfehler wird nicht aufgeraeumt und das Ziel
1064
+ bleibt stehen: „Noch einmal versuchen" ruft `loescheAlias`,
1065
+ und das beginnt mit `if (!S.loeschZiel) return;` — ohne Ziel
1066
+ war der Knopf eine Attrappe. Aufraeumen waere ohnehin falsch,
1067
+ solange noch etwas darunter liegt. */
1068
+ if (fehler.length) {
1069
+ zeichneBaum();
1070
+ zeichneErgebnis();
1071
+ return;
1072
+ }
923
1073
  leereEnumsRaeumen(enumsMitzunehmen, alle, function () {
924
1074
  leereOrdnerRaeumen(ziel, function () {
925
1075
  S.loeschZiel = null;
926
1076
  S.entwurf = null;
927
1077
  S.current = null;
1078
+ aboLoesen();
928
1079
  zeichneBaum();
929
1080
  zeichneErgebnis();
930
1081
  });
@@ -1174,11 +1325,28 @@ export function zeigeVerlegen(altVorgabe, zielVorgabe) {
1174
1325
  b.disabled = true;
1175
1326
  return;
1176
1327
  }
1177
- if (S.objects[neu]) {
1328
+ /* Ein Ziel, das es schon gibt — auch eines ohne eigenes
1329
+ Kanalobjekt. `S.objects` allein reicht nicht: legt man Aliaspunkte
1330
+ von Hand im Admin an, entsteht `alias.0.X.Y.PUNKT`, ohne dass
1331
+ `alias.0.X.Y` als Objekt existiert. Der Baum zeigt den Knoten
1332
+ trotzdem, und das Verlegen schrieb bisher mitten hinein und
1333
+ ueberschrieb vorhandene Punkte gleichen Namens (E31, T23). */
1334
+ if (knotenDa(neu)) {
1178
1335
  vorschau.appendChild(el('div', 'aside w', tr('move.exists', neu)));
1179
1336
  b.disabled = true;
1180
1337
  return;
1181
1338
  }
1339
+ /* Und kein Ziel, das mit der Quelle verschachtelt ist.
1340
+
1341
+ `alias.0.X` nach `alias.0.X.Sub`: `verlegeUmfang` enthaelt
1342
+ `alias.0.X`, und das Loeschen am Ende nimmt genau den Elternknoten
1343
+ des eben verlegten Alias mit. Umgekehrt genauso — dann laege das
1344
+ Ziel im Umfang und wuerde gleich wieder geloescht. */
1345
+ if (neu.indexOf(alt + '.') === 0 || alt.indexOf(neu + '.') === 0) {
1346
+ vorschau.appendChild(el('div', 'aside w', tr('move.nested')));
1347
+ b.disabled = true;
1348
+ return;
1349
+ }
1182
1350
  b.disabled = false;
1183
1351
 
1184
1352
  var umfang = verlegeUmfang(alt);
@@ -1360,7 +1528,7 @@ export function verlegeAlias(alt, neu) {
1360
1528
  blieb aus, und ein zweites Verlegen war nicht moeglich. */
1361
1529
  S.objects[zielId] = o;
1362
1530
  }
1363
- if (--offen === 0) { S.keysSorted = Object.keys(S.objects).sort(); weiter(); }
1531
+ if (--offen === 0) { indexNeu(); weiter(); }
1364
1532
  });
1365
1533
  });
1366
1534
 
@@ -1386,7 +1554,16 @@ export function verlegeAlias(alt, neu) {
1386
1554
  enumArbeit.forEach(function (a) {
1387
1555
  socket.emit('setObject', a.id, a.obj, function (err) {
1388
1556
  if (err) { fehler.push(a.id + ': ' + err); }
1389
- if (--offen2 === 0) { loeschen(); }
1557
+ if (--offen2 === 0) {
1558
+ /* Derselbe Abbruch wie eine Stufe darueber: geht etwas schief,
1559
+ wird nicht geloescht. Vorher lief `loeschen()` trotzdem —
1560
+ der alte Alias war weg, der neue stand in keiner Aufzaehlung,
1561
+ und die Zuordnung war damit verloren (gemessen 09.09.2026:
1562
+ `enum.rooms.WBP3` blieb leer zurueck). Zwei Aliase sind
1563
+ unschoen, aber reparierbar; eine verlorene Zuordnung nicht. */
1564
+ if (fehler.length) { return melden(fehler); }
1565
+ loeschen();
1566
+ }
1390
1567
  });
1391
1568
  });
1392
1569
  }
@@ -1400,7 +1577,7 @@ export function verlegeAlias(alt, neu) {
1400
1577
  if (err) { fehler.push(id + ': ' + err); }
1401
1578
  if (--offen3 === 0) {
1402
1579
  weg.forEach(function (x) { delete S.objects[x]; });
1403
- S.keysSorted = Object.keys(S.objects).sort();
1580
+ indexNeu();
1404
1581
  leereOrdnerRaeumen(alt, function () {
1405
1582
  S.current = neu;
1406
1583
  S.entwurf = null;
@@ -1428,9 +1605,15 @@ export function verlegeAlias(alt, neu) {
1428
1605
  body.appendChild(m);
1429
1606
  /* Nur in diesem Dialog verschwinden lassen - beim naechsten Oeffnen
1430
1607
  baut ihn `zeigeVerlegen` wieder auf. Vorher blieb er dauerhaft
1431
- versteckt, und ein zweites Verlegen ging nicht mehr. */
1432
- b.hidden = true;
1608
+ versteckt, und ein zweites Verlegen ging nicht mehr.
1609
+
1610
+ Nach einem Fehlschlag bleibt er als „Noch einmal versuchen" stehen:
1611
+ das Neue ist dann schon angelegt, das Alte steht noch, und ein
1612
+ zweiter Anlauf schreibt beides gefahrlos nach - genau das, was ein
1613
+ kurzer Aussetzer beim Schreiben der Aufzaehlung braucht. */
1433
1614
  b.disabled = false;
1615
+ if (f.length) { b.hidden = false; b.textContent = tr('write.retry'); }
1616
+ else { b.hidden = true; }
1434
1617
  ladeEnums(function () {
1435
1618
  zeichneBaum();
1436
1619
  /* Den Alias unter der neuen Kennung wieder aufschlagen.
@@ -1473,7 +1656,7 @@ export function leereOrdnerRaeumen(weg, fertig) {
1473
1656
  if (!o || o.type !== 'folder' || belegt) { return fertig(); }
1474
1657
  socket.emit('delObject', pfad, function () {
1475
1658
  delete S.objects[pfad];
1476
- S.keysSorted = Object.keys(S.objects).sort();
1659
+ indexNeu();
1477
1660
  weiter();
1478
1661
  });
1479
1662
  })();
@@ -1487,9 +1670,7 @@ export function schreibeObjekte() {
1487
1670
  var liste = [];
1488
1671
  var schlecht = 0;
1489
1672
  entwuerfe2.forEach(function (en) {
1490
- baueObjekte(en).forEach(function (o) {
1491
- if (!liste.some(function (x) { return x.id === o.id; })) { liste.push(o); }
1492
- });
1673
+ baueObjekte(en).forEach(function (o) { sammle(liste, o); });
1493
1674
  schlecht += pruefeSchreiben(en).length;
1494
1675
  });
1495
1676
  if (schlecht) { return; }
@@ -1560,7 +1741,11 @@ export function schreibeObjekte() {
1560
1741
  }
1561
1742
  $('#btn-dry-write').hidden = true;
1562
1743
  }
1563
- S.loeschListe = [];
1744
+ /* Nur bei Erfolg. Der Wiederholen-Knopf ruft `schreibeObjekte`
1745
+ direkt, und das filtert dann eine leere Liste: die verwaisten
1746
+ Punkte blieben fuer immer stehen, und die Erfolgsmeldung zaehlte
1747
+ sie nicht mehr mit (gemessen 09.09.2026). */
1748
+ if (!fehler.length) { S.loeschListe = []; }
1564
1749
 
1565
1750
  /* Die Marke „geaendert“ hat ihre Aufgabe erfuellt.
1566
1751
 
@@ -55,11 +55,27 @@ export function ladeSprache(fertig) {
55
55
  var beantwortet = false;
56
56
  socket.emit('getObject', 'system.config', function (err, obj) {
57
57
  if (beantwortet) { return; }
58
- beantwortet = true;
59
- /* Der Expertenmodus steht an derselben Stelle — einmal fragen reicht
60
- fuer beides. */
61
- expertenVorgabe = !!(!err && obj && obj.common && obj.common.expertMode);
58
+ /* Auch eine spaete Antwort gilt noch.
59
+
60
+ Nach 2,5 Sekunden geht es ohne weiter — richtig, sonst haengt der
61
+ Reiter an einem stillen Controller. Die Antwort danach wurde aber
62
+ ganz verworfen, und damit blieb `expertenVorgabe` auf „aus":
63
+ Ohne gesetzten `sessionStorage` fehlten alle Expertenobjekte im
64
+ Baum, ohne dass irgendwo stand, warum. Die Sprache umzustellen,
65
+ nachdem schon beschriftet ist, waere ein Sprung vor den Augen —
66
+ die bleibt beim Naechstbesten; der Expertenmodus wird nachgezogen
67
+ und der Baum neu gezeichnet, wenn er sich unterscheidet. */
68
+ var expNeu = !!(!err && obj && obj.common && obj.common.expertMode);
62
69
  var l = (!err && obj && obj.common && obj.common.language) || vorgabe;
70
+ if (beantwortet) {
71
+ if (expNeu !== expertenVorgabe) {
72
+ expertenVorgabe = expNeu;
73
+ if (typeof spaeterExperte === 'function') { spaeterExperte(); }
74
+ }
75
+ return;
76
+ }
77
+ beantwortet = true;
78
+ expertenVorgabe = expNeu;
63
79
  weiter(l);
64
80
  });
65
81
  setTimeout(function () {
@@ -67,6 +83,12 @@ export function ladeSprache(fertig) {
67
83
  }, 2500);
68
84
  }
69
85
 
86
+ /* Wer den Baum neu zeichnet, wenn der Expertenmodus spaet eintrifft.
87
+ Von aussen gesetzt, damit `sprache.js` nichts ueber den Baum wissen
88
+ muss. */
89
+ var spaeterExperte = null;
90
+ export function beiSpaetemExperten(fn) { spaeterExperte = fn; }
91
+
70
92
  /* Beschriftungen im festen HTML nachziehen. */
71
93
  export function beschrifteHtml() {
72
94
  document.title = tr('app.title');
@@ -133,8 +155,30 @@ export function sprachtext(v) {
133
155
  return String(v);
134
156
  }
135
157
 
158
+ /* Frueher stand hier `v.de || v.en` — die eingestellte Sprache kam
159
+ nicht vor. Auf einem englischen Admin standen mehrsprachig benannte
160
+ Objekte, Raeume und Funktionen deshalb deutsch da, waehrend
161
+ `sprachtext` eine Zeile darueber es richtig machte. Zwei Leser fuer
162
+ dieselbe Frage sind einer zu viel. */
136
163
  export function txt(v) {
137
- if (v === undefined || v === null) { return ''; }
138
- if (typeof v === 'object') { return v.de || v.en || ''; }
139
- return String(v);
164
+ return sprachtext(v);
165
+ }
166
+
167
+ /* Alle Sprachfassungen eines Namens, klein geschrieben.
168
+
169
+ Fuer den Abgleich mit Bestandsnamen: In der Anlage steht „Light",
170
+ von Hand getippt; die Vorlage sagt „Licht". Wer nur eine Fassung
171
+ vergleicht — und die auch noch nach Gross- und Kleinschreibung —,
172
+ legt eine zweite Funktion `enum.functions.light` neben die
173
+ vorhandene. */
174
+ export function alleTexte(v) {
175
+ if (v === undefined || v === null) { return []; }
176
+ var raus = [];
177
+ var dazu = function (x) {
178
+ var t = String(x || '').trim().toLowerCase();
179
+ if (t && raus.indexOf(t) === -1) { raus.push(t); }
180
+ };
181
+ if (typeof v === 'object') { Object.keys(v).forEach(function (k) { dazu(v[k]); }); }
182
+ else { dazu(v); }
183
+ return raus;
140
184
  }
package/admin/js/start.js CHANGED
@@ -4,8 +4,9 @@
4
4
  import { zeichneErgebnis } from './ergebnis.js';
5
5
  import { socket } from './verbindung.js';
6
6
  import { $ } from './basis.js';
7
- import { tr, ladeSprache, beschrifteHtml } from './sprache.js';
8
- import { ladeObjekte } from './objekte.js';
7
+ import { tr, ladeSprache, beschrifteHtml, beiSpaetemExperten } from './sprache.js';
8
+ import { ladeObjekte, abosErneuern } from './objekte.js';
9
+ import { zeichneBaum } from './baum.js';
9
10
  import { ladeBefehlswissen } from './mqtt.js';
10
11
  import { ladeVorlagen, setzeMeta } from './vorlagen.js';
11
12
  import { ladeMusterNamen } from './musternamen.js';
@@ -13,6 +14,11 @@ import { S } from './zustand.js';
13
14
 
14
15
 
15
16
  /* ================== Verbindung ================== */
17
+ /* Kommt die Expertenvorgabe erst nach dem Zeitablauf, muss der Baum
18
+ noch einmal gezeichnet werden — sonst fehlen die Expertenobjekte, bis
19
+ jemand den Reiter neu laedt. */
20
+ beiSpaetemExperten(function () { zeichneBaum(); });
21
+
16
22
  socket.on('connect', function () {
17
23
  var c = $('#conn');
18
24
  c.className = 'conn on';
@@ -32,6 +38,30 @@ socket.on('connect', function () {
32
38
  });
33
39
  });
34
40
  });
41
+ /* `connect` feuert genau einmal pro Seitenleben.
42
+
43
+ Der Reiter laedt nicht socket.io, sondern den WebSocket-Shim des
44
+ Admin (`ioBroker WebSockets`, bluefox). Dort steht woertlich:
45
+ `this.wasConnected ? this.reconnectHandlers.forEach(…)
46
+ : (this.connectHandlers.forEach(…), this.wasConnected = true)`.
47
+ Nach dem ersten Mal kommt also nur noch `reconnect` — und das hoerte
48
+ niemand. Folge jedes Aussetzers (Admin-Neustart, Netzwerkhaenger,
49
+ Standby): der Punkt blieb auf „getrennt" stehen, obwohl die
50
+ Verbindung wieder stand, und weder `objectChange` noch `stateChange`
51
+ kamen jemals wieder an (gemessen 09.09.2026 mit einem echten
52
+ `socket.close()`).
53
+
54
+ Neu geladen wird der ganze Bestand, nicht nur nachabonniert:
55
+ waehrend der Trennung kann sich alles geaendert haben, und ohne Abo
56
+ hat es niemand mitbekommen. Sprache, Geraetewissen und Vorlagen
57
+ bleiben, wie sie sind — die aendern sich nicht im Betrieb. */
58
+ socket.on('reconnect', function () {
59
+ var c = $('#conn');
60
+ c.className = 'conn on';
61
+ c.textContent = tr('app.connected');
62
+ abosErneuern();
63
+ ladeObjekte();
64
+ });
35
65
  socket.on('disconnect', function () {
36
66
  var c = $('#conn');
37
67
  c.className = 'conn off';
@@ -112,6 +112,15 @@ export function ladeVorlagen(fertig) {
112
112
  sie behauptet dann nicht, es gaebe keine eigenen Vorlagen. */
113
113
  setTimeout(function () { schluss([], true); }, 6000);
114
114
  socket.emit('getObject', INSTANZ_ID, function (err, o) {
115
+ /* Denselben Weg wie der Timeout eine Zeile darueber: ein Fehler
116
+ heisst nicht „keine eigenen Vorlagen".
117
+
118
+ `err` wurde vorher nicht einmal angesehen. Zwei Folgen: in der
119
+ Kopfzeile fehlte der Warnchip, und `freierSchluessel` hielt
120
+ vergebene Kennungen fuer frei — beim naechsten Speichern haette
121
+ `aendereVorlagen` eine vorhandene Vorlage mit Version 1
122
+ ersetzt. */
123
+ if (err) { return schluss([], true); }
115
124
  schluss((o && o.native && o.native.vorlagen) || []);
116
125
  });
117
126
  }
@@ -295,6 +304,47 @@ function punktErfuellt(kanal, pfad) {
295
304
  }
296
305
 
297
306
  /* Passt die Vorlage auf diesen Kanal? Liefert null oder eine Bewertung. */
307
+ /* Der Namenshinweis als Ausdruck — und der wirft nicht.
308
+
309
+ Der Text kommt frei aus dem Vorlagenblatt und aus dem
310
+ Speichern-Dialog; keins der beiden Felder prueft ihn. Ein `Lampe (Bad`
311
+ liess `pruefeVorlage` bei JEDER Geraeteauswahl werfen, kein Aufrufer
312
+ fing das, und die rechte Seite blieb leer — zu reparieren nur ueber
313
+ die Instanzkonfiguration von Hand (gemessen 09.09.2026). `rolleTrifft`
314
+ in `erkennung.js` faengt denselben Fall laengst ab; hier fehlte es.
315
+
316
+ Ein unbrauchbarer Ausdruck heisst „trifft nicht": der Hinweis ist nur
317
+ ein Bonus in der Bewertung, keine Bedingung. Die Vorlage bleibt damit
318
+ brauchbar, statt das ganze Werkzeug mitzureissen. */
319
+ export function hinweisTrifft(hinweis, name) {
320
+ if (!hinweisTaugt(hinweis)) { return false; }
321
+ try { return new RegExp(hinweis, 'i').test(name); }
322
+ catch { return false; }
323
+ }
324
+
325
+ /* Verschachtelte Wiederholungen — `(a+)+`, `(a*)*`, `(a+){2,}`.
326
+
327
+ Die klassische Form, an der eine RegExp-Maschine in die Knie geht:
328
+ `(a+)+$` gegen neunundzwanzig "a" brauchte hier gemessene 3196 ms,
329
+ und der ganze Reiter stand solange (09.09.2026). Ein try/catch faengt
330
+ das nicht — die Syntax ist ja in Ordnung, es dauert nur ewig.
331
+
332
+ Das ist eine Heuristik, kein Beweis: Sie deckt die Form ab, die man
333
+ sich versehentlich baut, nicht jeden denkbaren Fall. Fuer einen
334
+ Namenshinweis reicht das — er soll ein Wort im Geraetenamen finden,
335
+ nicht eine Sprache parsen. */
336
+ function verschachtelt(hinweis) {
337
+ return /\([^()]*[+*][^()]*\)\s*([+*]|\{\s*[0-9]+\s*,)/.test(String(hinweis));
338
+ }
339
+
340
+ /* Taugt der Text als Ausdruck? Fuer die Felder, die ihn aufnehmen. */
341
+ export function hinweisTaugt(hinweis) {
342
+ if (!hinweis) { return true; }
343
+ if (verschachtelt(hinweis)) { return false; }
344
+ try { new RegExp(hinweis, 'i'); return true; }
345
+ catch { return false; }
346
+ }
347
+
298
348
  export function pruefeVorlage(v, kanal, instanz) {
299
349
  var erk = v.erkennung || {};
300
350
  var gruende = [];
@@ -367,7 +417,7 @@ export function pruefeVorlage(v, kanal, instanz) {
367
417
  var belege = (erk.erforderlich || []).length *
368
418
  (v.mehrfach ? Math.max(1, ausgaenge.length) : 1) +
369
419
  Object.keys(erk.inhalt || {}).length;
370
- var namenstreffer = !!(erk.namenshinweis && new RegExp(erk.namenshinweis, 'i').test(name));
420
+ var namenstreffer = !!(erk.namenshinweis && hinweisTrifft(erk.namenshinweis, name));
371
421
  if (namenstreffer) { gruende.push(tr('tpl.nameHint', sprachtext(v.name))); }
372
422
  var eigen = v._quelle === 'benutzer';
373
423
  var punkte = belege * 1000000 +
@@ -469,6 +519,34 @@ export function wendeAn(v, kanal, gruende, instanz, auchOhneTreffer) {
469
519
  break;
470
520
  }
471
521
 
522
+ /* Ein Tastendruck hat keinen Zustand zum Lesen.
523
+
524
+ `STOP` am Rollladen traegt nur `schreiben` — genau so steht es in
525
+ der Vorlage, mit Begruendung. Diesen Fall kannte das Anwenden
526
+ nicht: Ohne `lesen` fiel der Punkt mit „STOP: undefined fehlt"
527
+ heraus, und KEIN EINZIGER Rollladen-Alias bekam je einen STOP
528
+ (gefunden 09.09.2026). Der Platz im Muster verlangt auch nichts
529
+ weiter als `write: true`. */
530
+ var nurSchreiben = !z.lesen && !!z.schreiben;
531
+ if (nurSchreiben && !quelle) {
532
+ var ziel0 = schreibZiel(z, kanal, auf);
533
+ if (!ziel0) {
534
+ e.uebersprungen.push(tr('tpl.skippedSource', z.name, auf(z.schreiben)));
535
+ return;
536
+ }
537
+ e.states.push({
538
+ n: z.name, on: !z.vorgabeAus, role: z.rolle || '', typ: z.typ || '',
539
+ unit: z.einheit || '', wr: true,
540
+ srcR: '', srcW: ziel0,
541
+ f: '', fw: z.schreibformel ? auf(z.schreibformel) : '',
542
+ caption: z.beschriftung || '',
543
+ dec: z.nachkommastellen === undefined ? undefined : String(z.nachkommastellen),
544
+ states: z.werteliste || undefined,
545
+ manuell: false, ausVorlage: z.name, hinweis: z.hinweis || '', wege: []
546
+ });
547
+ return;
548
+ }
549
+
472
550
  if (!quelle) {
473
551
  /* Beide Wege nennen, nicht nur den letzten — sonst sucht man an der
474
552
  einen Stelle und die Vorlage hatte an zwei geschaut. */
@@ -481,7 +559,15 @@ export function wendeAn(v, kanal, gruende, instanz, auchOhneTreffer) {
481
559
  /* Auch in der Formel steht der Platzhalter — ohne Ersetzung suchte
482
560
  der Alias nach einem Feld namens POWER%N%. */
483
561
  var formel = leseformel ? auf(leseformel) : '';
484
- if (feld) {
562
+ /* Wer eine Formel hinschreibt, bekommt sie auch.
563
+
564
+ `tasmota-rgb` fuehrt an FADE beides: `feld` UND `leseformel`. Die
565
+ Formel wurde verworfen und aus dem Feld eine neue gebaut — bei
566
+ FADE faellt es nicht auf, weil der Zieltyp die Umwandlung
567
+ nachholt, an der naechsten Zeile mit eigener Rechnung aber schon
568
+ (gefunden 09.09.2026). Das Feld bleibt der bequeme Weg; die
569
+ ausdrueckliche Angabe schlaegt ihn. */
570
+ if (feld && !formel) {
485
571
  feld = auf(feld);
486
572
  /* Mit Fragezeichen-Zugriff: `tele.SENSOR` ist ein Sammeltopf, und
487
573
  nicht jede Nachricht darin fuehrt jedes Feld. Ohne Schutz wirft
@@ -563,8 +649,10 @@ export function vorschlag(kanal, vorlagenId, instanz) {
563
649
  e.erzwungen = true;
564
650
  e.grund = e.grund.concat(gewaehlt.fehler.map(function (f) { return tr('tpl.wouldNeed', f); }));
565
651
  }
566
- e.andere = treffer.filter(function (t) { return t.vorlage.id !== gewaehlt.vorlage.id; })
567
- .map(function (t) { return t.vorlage; });
652
+ /* Hier stand `e.andere` — die Liste der unterlegenen Vorlagen. Sie
653
+ wurde bei jedem Vorschlag gebaut und von niemandem gelesen
654
+ (gefunden 09.09.2026). Was der Nutzer wirklich braucht, steht im
655
+ Musterwahl-Feld und im Vorlagenblatt. */
568
656
  if (vorlagenId) { e.vonHandGewaehlt = true; }
569
657
  return merkeHakenVorgabe(e);
570
658
  }