iobroker.alias-workbench 0.8.0

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.
Files changed (58) hide show
  1. package/LICENSE +21 -0
  2. package/README.md +516 -0
  3. package/admin/alias-workbench.png +0 -0
  4. package/admin/css/werkbank.css +916 -0
  5. package/admin/detector.js +1 -0
  6. package/admin/geraetewissen/tasmota-befehle.json +116 -0
  7. package/admin/i18n/de.json +552 -0
  8. package/admin/i18n/en.json +552 -0
  9. package/admin/js/aufzaehlungen.js +332 -0
  10. package/admin/js/basis.js +36 -0
  11. package/admin/js/baum.js +453 -0
  12. package/admin/js/detail.js +335 -0
  13. package/admin/js/einstellungen.js +40 -0
  14. package/admin/js/entwurf.js +908 -0
  15. package/admin/js/enums.js +23 -0
  16. package/admin/js/ergebnis.js +934 -0
  17. package/admin/js/erkennung.js +126 -0
  18. package/admin/js/katalog.js +313 -0
  19. package/admin/js/mqtt.js +885 -0
  20. package/admin/js/musternamen.js +94 -0
  21. package/admin/js/musterwahl.js +334 -0
  22. package/admin/js/objekte.js +243 -0
  23. package/admin/js/pruefungen.js +130 -0
  24. package/admin/js/quellentausch.js +570 -0
  25. package/admin/js/rollenwahl.js +199 -0
  26. package/admin/js/schreiben.js +1537 -0
  27. package/admin/js/sprache.js +137 -0
  28. package/admin/js/start.js +39 -0
  29. package/admin/js/verbindung.js +10 -0
  30. package/admin/js/vorlagen.js +572 -0
  31. package/admin/js/vorlagenblatt.js +1907 -0
  32. package/admin/js/vorschlagen.js +391 -0
  33. package/admin/js/werte.js +193 -0
  34. package/admin/js/zielleiste.js +339 -0
  35. package/admin/js/zuordnung.js +695 -0
  36. package/admin/js/zustand.js +47 -0
  37. package/admin/js/zustandsliste.js +515 -0
  38. package/admin/jsonConfig.json +249 -0
  39. package/admin/tab.html +148 -0
  40. package/admin/vorlagen/hm-dimmer.json +204 -0
  41. package/admin/vorlagen/hm-fensterkontakt-hm.json +176 -0
  42. package/admin/vorlagen/hm-fensterkontakt-hmip.json +191 -0
  43. package/admin/vorlagen/hm-heizgruppe.json +214 -0
  44. package/admin/vorlagen/hm-heizkoerper.json +236 -0
  45. package/admin/vorlagen/hm-rollladen.json +190 -0
  46. package/admin/vorlagen/hm-schaltaktor-lampe.json +197 -0
  47. package/admin/vorlagen/hm-schaltaktor-mehrfach.json +203 -0
  48. package/admin/vorlagen/hm-schaltaktor-steckdose.json +197 -0
  49. package/admin/vorlagen/hm-temperatur-feuchte.json +166 -0
  50. package/admin/vorlagen/hm-wandthermostat.json +228 -0
  51. package/admin/vorlagen/index.json +21 -0
  52. package/admin/vorlagen/messpunkt.json +225 -0
  53. package/admin/vorlagen/tasmota-lampe.json +263 -0
  54. package/admin/vorlagen/tasmota-mehrfach.json +203 -0
  55. package/admin/vorlagen/tasmota-rgb.json +239 -0
  56. package/admin/vorlagen/tasmota-steckdose.json +265 -0
  57. package/io-package.json +122 -0
  58. package/package.json +48 -0
@@ -0,0 +1,570 @@
1
+ /* Die Quelle eines Alias austauschen.
2
+
3
+ Der Fall: Ein Geraet geht kaputt und wird ersetzt. Der Alias soll
4
+ bleiben, wie er ist - dieselbe Kennung, dieselbe Aufzeichnung in der
5
+ Datenbank, dieselbe Zuordnung zu Raum und Funktion -, nur eben auf das
6
+ neue Geraet zeigen (Ricardos Punkt 17).
7
+
8
+ Geaendert wird deshalb so wenig wie moeglich: je Punkt `common.alias.id`
9
+ und am Kanal `native.quelle`. Alles andere bleibt unberuehrt. Das ist
10
+ der ganze Unterschied zum Neu-Anlegen, das den Alias aus der Vorlage
11
+ heraus neu baut und dabei Namen, Rollen und Formeln mitbringt.
12
+
13
+ Zugeordnet wird in drei Stufen, absteigend nach Verlaesslichkeit:
14
+ 1. gleicher relativer Pfad (…ABC.1.STATE → …XYZ.1.STATE)
15
+ 2. ueber die Vorlage (der Punkt weiss aus `native.ausZustand`,
16
+ aus welcher Zeile er stammt)
17
+ 3. ueber Rolle und Namen (nur als Vorschlag, sichtbar markiert)
18
+ Was nichts findet, bleibt leer - und laesst sich dann entfernen. */
19
+
20
+ import { S } from './zustand.js';
21
+ import { $, el } from './basis.js';
22
+ import { tr, txt } from './sprache.js';
23
+ import { socket } from './verbindung.js';
24
+ import { aliasQuellen, kindZustaende } from './werte.js';
25
+ import { holeZweig, indexNeu } from './objekte.js';
26
+ import { waehle } from './entwurf.js';
27
+ import { zeichneErgebnis } from './ergebnis.js';
28
+
29
+ /* Die Punkte des Alias, die ueberhaupt eine Quelle haben. */
30
+ function aliasPunkte(kanal) {
31
+ return S.keysSorted.filter(function (id) {
32
+ if (id.indexOf(kanal + '.') !== 0) { return false; }
33
+ var o = S.objects[id];
34
+ return o && o.type === 'state' && o.common && o.common.alias;
35
+ });
36
+ }
37
+
38
+ /* Woher liest dieser Alias heute? Der Kanal weiss es, wenn er von der
39
+ Werkbank stammt; sonst wird es aus den Punkten abgeleitet - die
40
+ laengste gemeinsame Wurzel aller Lesequellen. */
41
+ export function alteQuelle(kanal) {
42
+ var k = S.objects[kanal];
43
+ if (k && k.native && k.native.quelle) { return k.native.quelle; }
44
+ var wurzeln = [];
45
+ aliasPunkte(kanal).forEach(function (id) {
46
+ var q = aliasQuellen(S.objects[id]);
47
+ [q.read, q.write].forEach(function (x) {
48
+ if (!x) { return; }
49
+ /* Der Elternpfad, nicht der Datenpunkt selbst: bei einem Alias mit
50
+ nur einem Punkt waere die „gemeinsame Wurzel" sonst der Punkt,
51
+ und die Zuordnung ueber den relativen Pfad ginge ins Leere -
52
+ sichtbar als „vermutet" an einer Zeile, die sich gar nicht
53
+ aendert (Ricardo, 25.08.2026, an einem zigbee2mqtt-Alias). */
54
+ var e = x.slice(0, x.lastIndexOf('.'));
55
+ if (e.split('.').length > 2 && wurzeln.indexOf(e) === -1) { wurzeln.push(e); }
56
+ });
57
+ });
58
+ if (!wurzeln.length) { return ''; }
59
+ var teile = wurzeln[0].split('.');
60
+ for (var i = 1; i < wurzeln.length; i++) {
61
+ var t2 = wurzeln[i].split('.');
62
+ var n = 0;
63
+ while (n < teile.length && n < t2.length && teile[n] === t2[n]) { n++; }
64
+ teile = teile.slice(0, n);
65
+ }
66
+ return teile.length > 2 ? teile.join('.') : '';
67
+ }
68
+
69
+ /* Stufe 2: Was sagt die Vorlage? Der Punkt merkt sich in
70
+ `native.vorlage`/`native.ausZustand`, aus welcher Zeile er kam. */
71
+ function ausVorlage(punktId, neuKanal) {
72
+ var o = S.objects[punktId];
73
+ var nat = (o && o.native) || {};
74
+ if (!nat.vorlage || !nat.ausZustand) { return null; }
75
+ var v = null;
76
+ S.VORLAGEN.forEach(function (x) { if (x.id === nat.vorlage) { v = x; } });
77
+ if (!v) { return null; }
78
+ var zeile = null;
79
+ (v.zustaende || []).forEach(function (z) {
80
+ if (z.name === nat.ausZustand) { zeile = z; }
81
+ });
82
+ if (!zeile) { return null; }
83
+ var mach = function (rel) {
84
+ if (!rel) { return ''; }
85
+ var id = neuKanal + '.' + rel;
86
+ return S.objects[id] ? id : '';
87
+ };
88
+ var r = mach(zeile.lesen);
89
+ if (!r && zeile.lesenSonst) { r = mach(zeile.lesenSonst.punkt); }
90
+ return { read: r, write: mach(zeile.schreiben) };
91
+ }
92
+
93
+ /* Stufe 3: gleicher Punktname irgendwo unter dem neuen Geraet, oder
94
+ gleiche Rolle. Bewusst schwach - deshalb nur als Vorschlag. */
95
+ function geraten(altQuelle, neuKanal) {
96
+ var name = altQuelle.split('.').pop();
97
+ var kandidaten = kindZustaende(neuKanal);
98
+ var treffer = '';
99
+ kandidaten.forEach(function (id) {
100
+ if (!treffer && id.split('.').pop() === name) { treffer = id; }
101
+ });
102
+ if (treffer) { return treffer; }
103
+ var altO = S.objects[altQuelle];
104
+ var rolle = altO && altO.common && altO.common.role;
105
+ if (rolle) {
106
+ kandidaten.forEach(function (id) {
107
+ var o = S.objects[id];
108
+ if (!treffer && o && o.common && o.common.role === rolle) { treffer = id; }
109
+ });
110
+ }
111
+ return treffer;
112
+ }
113
+
114
+ /* Die Zuordnungstabelle: je Alias-Punkt, was heute gilt und was danach
115
+ gelten soll. `stufe` sagt, wie verlaesslich der Vorschlag ist. */
116
+ export function tauschPlan(kanal, neuKanal) {
117
+ var altK = alteQuelle(kanal);
118
+ return aliasPunkte(kanal).map(function (id) {
119
+ var q = aliasQuellen(S.objects[id]);
120
+ var zeile = { id: id, name: id.slice(kanal.length + 1),
121
+ altR: q.read || '', altW: (q.einfach ? '' : (q.write || '')),
122
+ einfach: !!q.einfach, neuR: '', neuW: '', stufe: 0 };
123
+ if (!neuKanal) { return zeile; }
124
+ /* 1. gleicher Pfad unterhalb der Quelle.
125
+
126
+ Mehrere Versuche, weil die alte Wurzel und der neu gewaehlte Knoten
127
+ nicht auf derselben Ebene liegen muessen: Wer bei einem Alias mit
128
+ einem einzigen Punkt das GERAET waehlt, waehrend die alte Wurzel
129
+ ein KANAL ist, soll trotzdem einen sauberen Treffer bekommen und
130
+ nicht die richtige Ebene raten muessen (Ricardo, 25.08.2026).
131
+ Reihenfolge: erst der volle relative Pfad, dann die letzten beiden
132
+ Glieder (`1.STATE`), zuletzt der blosse Punktname. */
133
+ var suchePfad = function (voll) {
134
+ var versuche = [];
135
+ if (altK && voll.indexOf(altK + '.') === 0) { versuche.push(voll.slice(altK.length + 1)); }
136
+ var tt = voll.split('.');
137
+ if (tt.length > 1) { versuche.push(tt.slice(-2).join('.')); }
138
+ versuche.push(tt[tt.length - 1]);
139
+ for (var i = 0; i < versuche.length; i++) {
140
+ var kand = neuKanal + '.' + versuche[i];
141
+ if (S.objects[kand]) { return kand; }
142
+ }
143
+ return '';
144
+ };
145
+ if (zeile.altR) {
146
+ var trR = suchePfad(zeile.altR);
147
+ if (trR) { zeile.neuR = trR; zeile.stufe = 1; }
148
+ }
149
+ if (zeile.altW) {
150
+ var trW = suchePfad(zeile.altW);
151
+ if (trW) { zeile.neuW = trW; }
152
+ }
153
+ /* 2. ueber die Vorlage */
154
+ if (!zeile.neuR) {
155
+ var v = ausVorlage(id, neuKanal);
156
+ if (v && v.read) { zeile.neuR = v.read; zeile.stufe = 2; if (v.write) { zeile.neuW = v.write; } }
157
+ }
158
+ /* 3. geraten */
159
+ if (!zeile.neuR && zeile.altR) {
160
+ var g = geraten(zeile.altR, neuKanal);
161
+ if (g) { zeile.neuR = g; zeile.stufe = 3; }
162
+ }
163
+ if (!zeile.neuW && zeile.altW) {
164
+ var gw = geraten(zeile.altW, neuKanal);
165
+ if (gw) { zeile.neuW = gw; }
166
+ }
167
+ /* Bei einfacher Quelle gilt die Lesequelle fuer beides. */
168
+ if (zeile.einfach && zeile.neuR) { zeile.neuW = zeile.neuR; }
169
+ return zeile;
170
+ });
171
+ }
172
+
173
+ /* Was der Tausch am Ende schreibt - dieselbe Form wie `baueObjekte`,
174
+ damit der Trockenlauf-Dialog sie unveraendert anzeigen kann. */
175
+ export function tauschObjekte(kanal, neuKanal, plan, weg) {
176
+ var raus = [];
177
+ var k = S.objects[kanal];
178
+ if (k) {
179
+ var kn = JSON.parse(JSON.stringify(k));
180
+ delete kn.ts; delete kn.from; delete kn.user; delete kn.acl; delete kn._id;
181
+ kn.native = kn.native || {};
182
+ kn.native.quelle = neuKanal;
183
+ raus.push({ id: kanal, neu: false, obj: kn });
184
+ }
185
+ plan.forEach(function (z) {
186
+ if (weg[z.id]) { return; }
187
+ if (!z.neuR) { return; }
188
+ var o = S.objects[z.id];
189
+ if (!o) { return; }
190
+ var neu = JSON.parse(JSON.stringify(o));
191
+ delete neu.ts; delete neu.from; delete neu.user; delete neu.acl; delete neu._id;
192
+ var a = neu.common.alias || {};
193
+ if (z.neuW && z.neuW !== z.neuR) { a.id = { read: z.neuR, write: z.neuW }; }
194
+ else { a.id = z.neuR; }
195
+ neu.common.alias = a;
196
+ raus.push({ id: z.id, neu: false, obj: neu });
197
+ });
198
+ return raus;
199
+ }
200
+
201
+ /* ================== Dialog ================== */
202
+
203
+ /* Wo koennte die neue Quelle liegen? Alles, worunter Datenpunkte haengen
204
+ und was nicht selbst ein Alias ist. Gefiltert wird ueber den Suchtext,
205
+ sonst waeren es auf einer grossen Anlage Tausende Zeilen. */
206
+ var quellenCache = null, quellenStand = -1;
207
+
208
+ /* Alle Stellen, unter denen Datenpunkte haengen - als Kandidaten fuer
209
+ eine neue Quelle. Nicht ueber `S.objects` filtern: MQTT legt fuer
210
+ Zwischenebenen GAR KEINE Objekte an, das halbe Haus faende sich sonst
211
+ nicht (derselbe Fallstrick wie im Baum, Testplan B24). Gesammelt wird
212
+ deshalb aus den Eltern der Datenpunkte selbst, einmal je Objektstand. */
213
+ function quellenListe() {
214
+ if (quellenCache && quellenStand === S.keysSorted.length) { return quellenCache; }
215
+ var da = {};
216
+ S.keysSorted.forEach(function (id) {
217
+ if (id.indexOf('alias.') === 0) { return; }
218
+ var o = S.objects[id];
219
+ if (!o || o.type !== 'state') { return; }
220
+ var p = id.slice(0, id.lastIndexOf('.'));
221
+ if (p.split('.').length > 2) { da[p] = true; }
222
+ /* Eine Ebene hoeher steht bei Homematic das Geraet ueber den Kanaelen. */
223
+ var p2 = p.slice(0, p.lastIndexOf('.'));
224
+ if (p2.split('.').length > 2) { da[p2] = true; }
225
+ });
226
+ quellenCache = Object.keys(da).sort();
227
+ quellenStand = S.keysSorted.length;
228
+ return quellenCache;
229
+ }
230
+
231
+ /* Der Auswahlbaum im Tauschdialog.
232
+
233
+ Eine flache Trefferliste war der falsche Weg: Wer eine Quelle sucht,
234
+ ist die Baumstruktur von links gewohnt (Ricardo, 25.08.2026). Gebaut
235
+ wird er hier eigens und nicht mit `zeichneBaum` - der zeichnet fest in
236
+ `#tree`, hängt an `S.baumModus` und ruft beim Klick `waehle()`. Ein
237
+ zweiter Baum mit eigenem Klappzustand und eigenem Klickziel ist
238
+ einfacher als diesen für zwei Aufgaben umzubauen.
239
+
240
+ Gezeigt wird nur, was als Quelle taugt: die Eltern von Datenpunkten
241
+ (quellenListe) und die Ebenen darüber als Wegweiser. */
242
+
243
+ var tOffen = {};
244
+
245
+ function baumDaten(filter) {
246
+ var liste = quellenListe();
247
+ var such = String(filter || '').trim().toLowerCase();
248
+ if (such) {
249
+ liste = liste.filter(function (id) {
250
+ if (id.toLowerCase().indexOf(such) > -1) { return true; }
251
+ var o = S.objects[id];
252
+ var n = txt(o && o.common && o.common.name) || '';
253
+ return n.toLowerCase().indexOf(such) > -1;
254
+ });
255
+ }
256
+ /* Verschachtelte Karte aus den Kennungen. `_w` merkt sich, ob dieser
257
+ Knoten selbst als Quelle taugt. */
258
+ var wurzel = { kinder: {}, _w: false };
259
+ liste.forEach(function (id) {
260
+ var teile = id.split('.');
261
+ var k = wurzel, pfad = '';
262
+ for (var i = 0; i < teile.length; i++) {
263
+ pfad = pfad ? pfad + '.' + teile[i] : teile[i];
264
+ /* Instanz (adapter.0) als eine Zeile, nicht als zwei. */
265
+ if (i === 0 && teile.length > 1 && /^\d+$/.test(teile[1])) {
266
+ pfad = teile[0] + '.' + teile[1];
267
+ i++;
268
+ }
269
+ if (!k.kinder[pfad]) { k.kinder[pfad] = { kinder: {}, _w: false, id: pfad }; }
270
+ k = k.kinder[pfad];
271
+ }
272
+ k._w = true;
273
+ });
274
+ return wurzel;
275
+ }
276
+
277
+ function zeichneAuswahlbaum(host, filter, beiWahl) {
278
+ host.textContent = '';
279
+ var wurzel = baumDaten(filter);
280
+ var such = String(filter || '').trim().length > 1;
281
+
282
+ var male = function (knoten, tiefe, behaelter) {
283
+ Object.keys(knoten.kinder).sort().forEach(function (pfad) {
284
+ var k = knoten.kinder[pfad];
285
+ var kinderNamen = Object.keys(k.kinder);
286
+ var hatKinder = kinderNamen.length > 0;
287
+ /* Bei einer Suche steht alles offen, sonst der gemerkte Zustand. */
288
+ var auf = such ? true : !!tOffen[k.id];
289
+ var zeile = el('div', 'node' + (k._w ? ' pick' : ' grp')
290
+ + (S.tauschNeu === k.id ? ' sel' : ''));
291
+ zeile.style.paddingLeft = (6 + tiefe * 13) + 'px';
292
+ var tw = el('span', 'tw', hatKinder ? (auf ? '▾' : '▸') : '');
293
+ if (hatKinder) {
294
+ tw.addEventListener('click', function (ev) {
295
+ ev.stopPropagation();
296
+ tOffen[k.id] = !auf;
297
+ zeichneAuswahlbaum(host, filter, beiWahl);
298
+ });
299
+ }
300
+ zeile.appendChild(tw);
301
+ /* Die Instanz steht als eine Zeile (`hm-rpc.0`), sonst waere die
302
+ oberste Ebene eine Reihe von Nullen. */
303
+ var teileK = k.id.split('.');
304
+ var kurz = (teileK.length === 2) ? k.id : teileK[teileK.length - 1];
305
+ zeile.appendChild(el('span', 'nm', kurz));
306
+ var o = S.objects[k.id];
307
+ var name = txt(o && o.common && o.common.name) || '';
308
+ if (name && name !== kurz) { zeile.appendChild(el('span', 'zusatz', name)); }
309
+ if (k._w) {
310
+ var n = kindZustaende(k.id).length;
311
+ if (n) { zeile.appendChild(el('span', 'cnt', String(n))); }
312
+ zeile.addEventListener('click', function () { beiWahl(k.id); });
313
+ }
314
+ behaelter.appendChild(zeile);
315
+ if (hatKinder && auf) { male(k, tiefe + 1, behaelter); }
316
+ });
317
+ };
318
+ male(wurzel, 0, host);
319
+ if (!host.childNodes.length) {
320
+ host.appendChild(el('div', 'empty', tr('swap.noDevice')));
321
+ }
322
+ }
323
+
324
+ export function zeigeTausch() {
325
+ /* Wie bei zeigeVerlegen und zeigeLoeschen: an der Quelle ist
326
+ S.current die Quelle, gemeint ist das Ziel des Entwurfs (T1). */
327
+ var ziel = (S.entwurf && (S.entwurf.ziel || S.entwurf.kanal)) || S.current;
328
+ if (!ziel || ziel.indexOf('alias.') !== 0 || !S.objects[ziel]) { return; }
329
+ S.tauschZiel = ziel;
330
+ S.tauschNeu = '';
331
+ S.tauschWeg = {};
332
+ S.tauschPlan = null;
333
+ /* Auch die Suche faengt neu an. Sie blieb sonst stehen, und beim
334
+ naechsten Oeffnen zeigte der Baum „Kein Geraet gefunden" — im
335
+ schlimmsten Fall genau dann, wenn im Feld noch das eben getauschte,
336
+ inzwischen geloeschte Geraet stand (W13, gemessen 26.08.2026). */
337
+ S.tauschFilter = '';
338
+ var b = $('#btn-swap-go');
339
+ if (b) { b.hidden = false; b.textContent = tr('swap.now'); }
340
+ zeichneTausch();
341
+ var d = $('#dlg-swap');
342
+ if (d && !d.open) { d.showModal(); }
343
+ }
344
+
345
+ function zeichneTausch() {
346
+ var body = $('#swap-body');
347
+ if (!body) { return; }
348
+ body.textContent = '';
349
+ var kanal = S.tauschZiel;
350
+ var alt = alteQuelle(kanal);
351
+
352
+ /* Woher es heute liest - und ob es das noch gibt. */
353
+ var kopf = el('div', 'aside w');
354
+ kopf.style.marginBottom = '11px';
355
+ var z1 = el('div');
356
+ z1.appendChild(el('b', null, tr('swap.from') + ' '));
357
+ z1.appendChild(el('span', 'sn', alt || tr('swap.unknown')));
358
+ kopf.appendChild(z1);
359
+ if (alt && !S.objects[alt] && !kindZustaende(alt).length) {
360
+ kopf.appendChild(el('div', 'hint', tr('swap.gone')));
361
+ }
362
+ body.appendChild(kopf);
363
+
364
+ /* Das neue Geraet: Filter und darunter der Baum - dieselbe Bedienung
365
+ wie links, statt einer flachen Trefferliste (Ricardo, 25.08.2026). */
366
+ var lab = el('label', 'feld');
367
+ lab.appendChild(el('span', 'feldlabel', tr('swap.to')));
368
+ var feld = el('input', 'tx w');
369
+ feld.type = 'text';
370
+ feld.value = S.tauschFilter || '';
371
+ feld.placeholder = tr('swap.search');
372
+ lab.appendChild(feld);
373
+ body.appendChild(lab);
374
+
375
+ var baumHost = el('div', 'tree tauschbaum');
376
+ body.appendChild(baumHost);
377
+
378
+ var gewaehlt = function (id) {
379
+ S.tauschNeu = id;
380
+ S.tauschPlan = null;
381
+ S.tauschWeg = {};
382
+ zeichneTausch();
383
+ };
384
+ zeichneAuswahlbaum(baumHost, S.tauschFilter, gewaehlt);
385
+ feld.addEventListener('input', function () {
386
+ S.tauschFilter = feld.value;
387
+ zeichneAuswahlbaum(baumHost, S.tauschFilter, gewaehlt);
388
+ });
389
+
390
+ var go = $('#btn-swap-go');
391
+ if (!S.tauschNeu) { if (go) { go.disabled = true; } return; }
392
+ if (S.tauschNeu === alt) {
393
+ body.appendChild(el('div', 'aside w', tr('swap.same')));
394
+ if (go) { go.disabled = true; }
395
+ return;
396
+ }
397
+
398
+ /* Die Zuordnung, Zeile fuer Zeile. */
399
+ if (!S.tauschPlan) { S.tauschPlan = tauschPlan(kanal, S.tauschNeu); }
400
+ var plan = S.tauschPlan;
401
+ var k = el('div', 'card');
402
+ k.style.marginTop = '11px';
403
+ var ch = el('div', 'ch');
404
+ ch.appendChild(el('span', 'typ', tr('swap.mapping')));
405
+ var ohne = plan.filter(function (z) { return !z.neuR; }).length;
406
+ ch.appendChild(el('span', 'chip ok', String(plan.length - ohne)));
407
+ if (ohne) { ch.appendChild(el('span', 'chip bad', String(ohne))); }
408
+ k.appendChild(ch);
409
+
410
+ plan.forEach(function (z) {
411
+ var r = el('div', 'slot');
412
+ r.style.gridTemplateColumns = '120px 1fr';
413
+ /* Trifft der Vorschlag genau das, was schon dasteht, aendert sich an
414
+ dieser Zeile nichts - dann waere „vermutet" irrefuehrend
415
+ (Ricardo, 25.08.2026). */
416
+ /* Bei einer einfachen Quelle (ein Punkt fuer Lesen und Schreiben)
417
+ fuehrt der Plan `altW` leer und `neuW` gefuellt - geschrieben wird
418
+ am Ende aber in beiden Faellen nur `alias.id = <Punkt>`. Wer die
419
+ beiden trotzdem vergleicht, findet immer einen Unterschied, und die
420
+ Zeile bekam nie „unveraendert", obwohl sich nichts aendert
421
+ (gemessen 26.08.2026 am Dimmer-Alias, dessen ACTUAL schon auf die
422
+ neue Quelle zeigte). */
423
+ var gleich = z.neuR && z.neuR === z.altR
424
+ && (z.einfach || (z.neuW || '') === (z.altW || ''));
425
+ var nm = el('span', 'sn', z.name);
426
+ if (gleich) { nm.appendChild(el('span', 'rat', tr('swap.unchanged'))); }
427
+ else if (z.stufe === 3) { nm.appendChild(el('span', 'rat', tr('guess.mark'))); }
428
+ r.appendChild(nm);
429
+ var rechts = el('span');
430
+ /* Beschriftet, sonst raet man, welche der beiden Zeilen die neue ist. */
431
+ var paarZeile = function (beschriftung, text, klasse, farbe) {
432
+ var zz = el('div');
433
+ zz.style.display = 'grid';
434
+ zz.style.gridTemplateColumns = '52px 1fr';
435
+ zz.style.alignItems = 'baseline';
436
+ zz.appendChild(el('span', 'rx', beschriftung));
437
+ var w = el('span', klasse, text);
438
+ if (farbe) { w.style.color = farbe; }
439
+ zz.appendChild(w);
440
+ return zz;
441
+ };
442
+ rechts.appendChild(paarZeile(tr('write.diffOld'),
443
+ z.altR + (z.altW && z.altW !== z.altR ? ' → ' + z.altW : ''), 'zq'));
444
+ if (z.neuR) {
445
+ rechts.appendChild(paarZeile(tr('write.diffNew'),
446
+ z.neuR + (z.neuW && z.neuW !== z.neuR ? ' → ' + z.neuW : ''),
447
+ 'sn', gleich ? 'var(--ink-3)' : 'var(--ok)'));
448
+ } else {
449
+ var fehlt = el('div');
450
+ fehlt.style.color = 'var(--bad)';
451
+ fehlt.textContent = tr('swap.noMatch');
452
+ rechts.appendChild(fehlt);
453
+ /* Was nichts findet, laesst sich mit entfernen - sonst bliebe ein
454
+ Punkt zurueck, der ins Leere zeigt (Ricardos Wahl, 25.08.2026). */
455
+ var lb = document.createElement('label');
456
+ lb.style.display = 'flex';
457
+ lb.style.gap = '6px';
458
+ lb.style.alignItems = 'center';
459
+ lb.style.marginTop = '3px';
460
+ lb.style.cursor = 'pointer';
461
+ var cb = document.createElement('input');
462
+ cb.type = 'checkbox';
463
+ cb.checked = !!S.tauschWeg[z.id];
464
+ cb.addEventListener('change', function () { S.tauschWeg[z.id] = cb.checked; });
465
+ lb.appendChild(cb);
466
+ lb.appendChild(el('span', 'hint', tr('swap.removeIt')));
467
+ rechts.appendChild(lb);
468
+ }
469
+ r.appendChild(rechts);
470
+ k.appendChild(r);
471
+ });
472
+ body.appendChild(k);
473
+
474
+ /* Wo die neue Quelle anders tickt als die alte. Die Formel des Alias
475
+ bleibt beim Tausch stehen - rechnet die alte Quelle 0..100 und die
476
+ neue 0..255, stimmt danach jeder Wert nicht mehr. Das sieht man dem
477
+ Pfad nicht an, also steht es hier (Ricardos Szenario 8). */
478
+ var stolper = [];
479
+ plan.forEach(function (z) {
480
+ if (!z.neuR) { return; }
481
+ var a = S.objects[z.altR], b = S.objects[z.neuR];
482
+ if (!a || !b || !a.common || !b.common) { return; }
483
+ var ca = a.common, cb = b.common;
484
+ var was = [];
485
+ if (ca.type && cb.type && ca.type !== cb.type) {
486
+ was.push(tr('swap.diffType', ca.type, cb.type));
487
+ }
488
+ if ((ca.min !== undefined || ca.max !== undefined ||
489
+ cb.min !== undefined || cb.max !== undefined) &&
490
+ (ca.min !== cb.min || ca.max !== cb.max)) {
491
+ was.push(tr('swap.diffRange',
492
+ (ca.min === undefined ? '?' : ca.min) + '..' + (ca.max === undefined ? '?' : ca.max),
493
+ (cb.min === undefined ? '?' : cb.min) + '..' + (cb.max === undefined ? '?' : cb.max)));
494
+ }
495
+ if ((ca.unit || cb.unit) && ca.unit !== cb.unit) {
496
+ was.push(tr('swap.diffUnit', ca.unit || '—', cb.unit || '—'));
497
+ }
498
+ if (was.length) { stolper.push(z.name + ': ' + was.join(', ')); }
499
+ });
500
+ if (stolper.length) {
501
+ var sw = el('div', 'aside w');
502
+ sw.style.borderLeftColor = 'var(--warn)';
503
+ sw.appendChild(el('b', null, tr('swap.mind')));
504
+ stolper.forEach(function (x) { sw.appendChild(el('div', 'hint', x)); });
505
+ sw.appendChild(el('div', 'hint', tr('swap.mindHint')));
506
+ body.appendChild(sw);
507
+ }
508
+
509
+ /* Was gleich bleibt - der eigentliche Grund fuer diesen Weg. */
510
+ body.appendChild(el('div', 'aside w', tr('swap.keeps')));
511
+ if (plan.some(function (z) { return z.stufe === 3; })) {
512
+ body.appendChild(el('div', 'aside', tr('swap.guessHint')));
513
+ }
514
+ if (go) { go.disabled = false; }
515
+ }
516
+
517
+ export function tauscheAus() {
518
+ var kanal = S.tauschZiel, neu = S.tauschNeu;
519
+ if (!kanal || !neu || !S.tauschPlan) { return; }
520
+ var b = $('#btn-swap-go');
521
+ b.disabled = true;
522
+ b.textContent = tr('write.writing');
523
+
524
+ var arbeit = tauschObjekte(kanal, neu, S.tauschPlan, S.tauschWeg);
525
+ var loeschen = Object.keys(S.tauschWeg).filter(function (id) { return S.tauschWeg[id]; });
526
+ var fehler = [];
527
+
528
+ var fertig = function () {
529
+ var body = $('#swap-body');
530
+ body.textContent = '';
531
+ if (fehler.length) {
532
+ body.appendChild(el('div', 'aside w', fehler.join(' | ')));
533
+ b.disabled = false;
534
+ b.textContent = tr('write.retry');
535
+ return;
536
+ }
537
+ body.appendChild(el('div', 'aside w', tr('swap.done', neu)));
538
+ b.hidden = true;
539
+ holeZweig(neu, function () { waehle(kanal); zeichneErgebnis(); });
540
+ };
541
+
542
+ var weiter = function () {
543
+ if (!loeschen.length) { return fertig(); }
544
+ var offen2 = loeschen.length;
545
+ loeschen.forEach(function (id) {
546
+ socket.emit('delObject', id, function (err) {
547
+ if (err) { fehler.push(id + ': ' + err); }
548
+ else { delete S.objects[id]; }
549
+ if (--offen2 === 0) { indexNeu(); fertig(); }
550
+ });
551
+ });
552
+ };
553
+
554
+ var offen = arbeit.length;
555
+ if (!offen) { return weiter(); }
556
+ arbeit.forEach(function (a) {
557
+ socket.emit('setObject', a.id, a.obj, function (err) {
558
+ if (err) { fehler.push(a.id + ': ' + err); }
559
+ else { S.objects[a.id] = a.obj; }
560
+ if (--offen === 0) { weiter(); }
561
+ });
562
+ });
563
+ }
564
+
565
+ /* Der Knopf gilt fuer dasselbe wie Verlegen und Entfernen: einen Alias,
566
+ den es gibt. */
567
+ export function tauschKnopf(zeigen) {
568
+ var b = $('#btn-swap');
569
+ if (b) { b.hidden = !zeigen; }
570
+ }