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,1537 @@
1
+ /* Alles, was Objekte anfasst: bauen, pruefen, Trockenlauf, schreiben,
2
+ loeschen, verlegen. Geschrieben wird nur, was der Trockenlauf vorher
3
+ gezeigt hat. */
4
+
5
+ import { S } from './zustand.js';
6
+ import { socket } from './verbindung.js';
7
+ import { $, el } from './basis.js';
8
+ import { tr, txt } from './sprache.js';
9
+ import { enums, ladeEnums } from './enums.js';
10
+ import { enumsVon } from './aufzaehlungen.js';
11
+ import './katalog.js';
12
+ import { holeZweig, indexNeu , nachziehenErledigt } from './objekte.js';
13
+ import { zeichneBaum } from './baum.js';
14
+ import './werte.js';
15
+ import { ausgangName, vorschlag } from './vorlagen.js';
16
+ import {
17
+ waehle,
18
+ ordnerUnterAlias,
19
+ gemeinsameKanaele,
20
+ kennungtauglich,
21
+ entwurfFuer
22
+ } from './entwurf.js';
23
+ import { zeichneErgebnis } from './ergebnis.js';
24
+
25
+ import { uebernehmeBestand , enumAenderungen, setzeZiel } from './zuordnung.js';
26
+
27
+ import { mqttEinzelnStill } from './mqtt.js';
28
+
29
+ import { rateBehalten } from './vorschlagen.js';
30
+
31
+
32
+ export var EIGENE_COMMON = ['name', 'role', 'type', 'read', 'write', 'alias', 'unit', 'states'];
33
+
34
+ export function mitFremdem(id, common) {
35
+ var alt = S.objects[id] && S.objects[id].common;
36
+ if (!alt) { return common; }
37
+ Object.keys(alt).forEach(function (k) {
38
+ if (EIGENE_COMMON.indexOf(k) > -1) { return; }
39
+ if (common[k] !== undefined) { return; }
40
+ common[k] = JSON.parse(JSON.stringify(alt[k]));
41
+ });
42
+ return common;
43
+ }
44
+
45
+ /* Aendert dieses Objekt ueberhaupt etwas?
46
+
47
+ Beim Aktualisieren stand an jeder Zeile „ueberschreibt" — auch an den
48
+ neun von zehn, die hinterher Zeichen fuer Zeichen dasselbe enthielten.
49
+ Das las sich, als wuerde ein fertiger Alias bei jedem Durchgang neu
50
+ gebaut, und verdeckte die eine Zeile, in der wirklich etwas passiert.
51
+
52
+ Verglichen wird ohne die Felder, die der Controller selbst setzt:
53
+ Zeitstempel, Urheber, Rechte. Und mit sortierten Schluesseln — sonst
54
+ zaehlte eine andere Reihenfolge im JSON schon als Unterschied. */
55
+ export function stabil(o) {
56
+ if (o === null || typeof o !== 'object') { return o; }
57
+ if (Array.isArray(o)) { return o.map(stabil); }
58
+ var raus = {};
59
+ Object.keys(o).sort().forEach(function (k) { raus[k] = stabil(o[k]); });
60
+ return raus;
61
+ }
62
+ /* Zwei Textfassungen zeilenweise gegenueberstellen - wie ein
63
+ Vergleichswerkzeug es tut: links der Bestand, rechts was entstuende,
64
+ gleiche Zeilen auf gleicher Hoehe.
65
+
66
+ Erst die laengste gemeinsame Teilfolge (LCS) ueber die Zeilen, dann
67
+ werden Loesch- und Einfuegebloecke an derselben Stelle paarweise
68
+ nebeneinander gelegt - sonst stuende eine geaenderte Zeile zweimal
69
+ untereinander statt einmal daneben. Bei JSON-Bloecken von ein paar
70
+ Dutzend Zeilen ist die Tabelle winzig, das kostet nichts. */
71
+ export function zeilenVergleich(altText, neuText) {
72
+ var a = String(altText).split('\n'), b = String(neuText).split('\n');
73
+ var n = a.length, m = b.length, i, j;
74
+ /* Verglichen wird ohne das abschliessende Komma: es haengt nur davon
75
+ ab, ob nach der Zeile noch etwas folgt. Kommt ein Feld dazu, bekaeme
76
+ die Zeile davor sonst ein Komma und stuende als „geaendert“ da,
77
+ obwohl an ihr nichts anders ist. Angezeigt wird die Zeile samt
78
+ Komma - verglichen ohne. */
79
+ var kern = function (z) { return String(z).replace(/,$/, ''); };
80
+ var ak = a.map(kern), bk = b.map(kern);
81
+ var t = [];
82
+ for (i = 0; i <= n; i++) {
83
+ var reihe = [];
84
+ for (j = 0; j <= m; j++) { reihe.push(0); }
85
+ t.push(reihe);
86
+ }
87
+ for (i = n - 1; i >= 0; i--) {
88
+ for (j = m - 1; j >= 0; j--) {
89
+ t[i][j] = (ak[i] === bk[j]) ? t[i + 1][j + 1] + 1
90
+ : Math.max(t[i + 1][j], t[i][j + 1]);
91
+ }
92
+ }
93
+ var roh = [];
94
+ i = 0; j = 0;
95
+ while (i < n && j < m) {
96
+ if (ak[i] === bk[j]) { roh.push({ art: 'gleich', l: a[i], r: b[j] }); i++; j++; }
97
+ else if (t[i + 1][j] >= t[i][j + 1]) { roh.push({ art: 'weg', l: a[i] }); i++; }
98
+ else { roh.push({ art: 'dazu', r: b[j] }); j++; }
99
+ }
100
+ while (i < n) { roh.push({ art: 'weg', l: a[i++] }); }
101
+ while (j < m) { roh.push({ art: 'dazu', r: b[j++] }); }
102
+
103
+ var raus = [];
104
+ for (var k = 0; k < roh.length; k++) {
105
+ if (roh[k].art !== 'weg') { raus.push(roh[k]); continue; }
106
+ var wegs = [], dazus = [];
107
+ while (k < roh.length && roh[k].art === 'weg') { wegs.push(roh[k].l); k++; }
108
+ while (k < roh.length && roh[k].art === 'dazu') { dazus.push(roh[k].r); k++; }
109
+ k--;
110
+ var viele = Math.max(wegs.length, dazus.length);
111
+ for (var p = 0; p < viele; p++) {
112
+ if (p < wegs.length && p < dazus.length) { raus.push({ art: 'anders', l: wegs[p], r: dazus[p] }); }
113
+ else if (p < wegs.length) { raus.push({ art: 'weg', l: wegs[p] }); }
114
+ else { raus.push({ art: 'dazu', r: dazus[p] }); }
115
+ }
116
+ }
117
+ return raus;
118
+ }
119
+
120
+ /* Ein Objekt als JSON-Text fuer den Vergleich - ohne die Felder, die der
121
+ Controller selbst setzt (sonst haenge der Vergleich an Zeitstempeln).
122
+
123
+ `geordnet` sortiert die Schluessel auf beiden Seiten gleich. Das ist
124
+ keine Schoenung: die Reihenfolge in einem JSON-Objekt bedeutet nichts,
125
+ und sie ist auch keine stillschweigende Vorgabe - gemessen an Ricardos
126
+ Anlage schreibt derselbe Admin Alias-Objekte in VIER verschiedenen
127
+ Reihenfolgen (common,native,type / type,common,native /
128
+ native,type,common / common,type,native), javascript.0 in zwei.
129
+ Ohne Ordnen sieht ein Vergleich nach einem Dutzend Unterschieden aus,
130
+ wo zwei sind (Ricardo, 25.08.2026). Wer das Original sehen will,
131
+ schaltet um. */
132
+ /* Die Reihenfolge, in der die Doku die Felder auffuehrt - und in der sie
133
+ zusammengehoeren: `read` und `write` nebeneinander, `alias` als groesster
134
+ Block am Ende. Alphabetisch sortiert stuende `read` hinter `name` und
135
+ `write` hinter `unit`, also weit auseinander (Ricardo, 25.08.2026).
136
+ Was hier nicht steht - icon, color, desc, custom -, kommt dahinter,
137
+ alphabetisch. */
138
+ var VORRANG = {
139
+ '': ['type', 'common', 'native'],
140
+ common: ['name', 'role', 'type', 'read', 'write', 'unit', 'states', 'alias'],
141
+ 'common.alias': ['id', 'read', 'write']
142
+ };
143
+ function ordne(o, pfad) {
144
+ if (o === null || typeof o !== 'object') { return o; }
145
+ if (Array.isArray(o)) { return o.map(function (x) { return ordne(x, pfad); }); }
146
+ var vor = VORRANG[pfad] || [];
147
+ var raus = {};
148
+ Object.keys(o).sort(function (a, b) {
149
+ var ia = vor.indexOf(a), ib = vor.indexOf(b);
150
+ if (ia > -1 && ib > -1) { return ia - ib; }
151
+ if (ia > -1) { return -1; }
152
+ if (ib > -1) { return 1; }
153
+ return a < b ? -1 : (a > b ? 1 : 0);
154
+ }).forEach(function (k) { raus[k] = ordne(o[k], pfad ? pfad + '.' + k : k); });
155
+ return raus;
156
+ }
157
+
158
+ export function objektText(o, geordnet) {
159
+ if (!o) { return ''; }
160
+ var k = JSON.parse(JSON.stringify(o));
161
+ delete k.ts; delete k.from; delete k.user; delete k.acl; delete k._id;
162
+ return JSON.stringify(geordnet ? ordne(k, '') : k, null, 2);
163
+ }
164
+
165
+ export function unveraendert(id, obj) {
166
+ var alt = S.objects[id];
167
+ if (!alt) { return false; }
168
+ var putz = function (x) {
169
+ var k = JSON.parse(JSON.stringify(x || {}));
170
+ delete k.ts; delete k.from; delete k.user; delete k.acl; delete k._id;
171
+ return stabil(k);
172
+ };
173
+ return JSON.stringify(putz(alt)) === JSON.stringify(putz(obj));
174
+ }
175
+
176
+
177
+ /* ================== Objekte bauen, prüfen, schreiben ================== */
178
+
179
+ /* Aus dem Entwurf die Objekte machen, die entstehen wuerden. */
180
+ export function baueObjekte(e) {
181
+ var ziel = e.ziel || e.kanal;
182
+ var raus = [];
183
+
184
+ /* Ordner darueber, falls es ihn noch nicht gibt */
185
+ var teile = ziel.split('.');
186
+ /* Jede fehlende Ebene zwischen alias.0 und dem Geraet anlegen. */
187
+ for (var i = 3; i < teile.length; i++) {
188
+ var pfad = teile.slice(0, i).join('.');
189
+ if (!S.objects[pfad]) {
190
+ raus.push({ id: pfad, neu: true, obj: {
191
+ type: 'folder',
192
+ common: mitFremdem(pfad, { name: teile[i - 1] }),
193
+ native: {}
194
+ } });
195
+ }
196
+ }
197
+
198
+ /* Der Kanal — hier wird die Vorlage vermerkt, damit ein spaeteres
199
+ Nachziehen weiss, woraus das Geraet entstanden ist. */
200
+ /* Wird der Alias aus sich selbst heraus bearbeitet — Baummodus
201
+ „Aliase" —, darf „quelle" nicht auf ihn selbst zeigen. Vorher
202
+ verschwanden dabei Vorlage, Version und Geraetetyp, und damit die
203
+ ganze Herkunft: das Abzeichen „gemerkt" war weg, die Erkennung
204
+ nahm womoeglich eine andere Vorlage, und ein spaeteres Nachziehen
205
+ der Versionen haette nichts mehr zu greifen. */
206
+ var altN = (S.objects[ziel] && S.objects[ziel].native) || {};
207
+ var ausAlias = (e.kanal.indexOf('alias.') === 0);
208
+ var kanalNative = ausAlias ? JSON.parse(JSON.stringify(altN)) : { quelle: e.kanal };
209
+ if (e.vorlage) {
210
+ kanalNative.vorlage = e.vorlage;
211
+ kanalNative.vorlageVersion = e.vorlageVersion;
212
+ kanalNative.geraetetyp = e.want;
213
+ if (e.want !== e.wantAuto) { kanalNative.musterVonHand = true; }
214
+ }
215
+ /* Einen von Hand vergebenen Kanalnamen nicht durch die Kennung
216
+ ersetzen. */
217
+ var altName = txt(S.objects[ziel] && S.objects[ziel].common && S.objects[ziel].common.name);
218
+ /* Neu angelegt wird ein Kanal — das ist die verbreitete Form und die,
219
+ die dem Objektschema am naechsten kommt. Was es schon gibt, behaelt
220
+ aber seinen Typ: ein Aktualisieren soll nicht nebenbei aus einem
221
+ Ordner einen Kanal machen. Der Aliasmechanismus schaut ohnehin nur
222
+ auf die Datenpunkte, nicht auf ihren Behaelter. */
223
+ var altTyp = S.objects[ziel] && S.objects[ziel].type;
224
+ /* Die Rolle des Kanals ist der Geraetetyp - so haelt es der
225
+ Geraete-Adapter des Admin, und so steht es in Ricardos Anlage:
226
+ 115 von 118 Alias-Kanaelen tragen einen Detector-Typ als Rolle
227
+ (socket, blind, light, thermostat ...). Wir setzten sie nie, und
228
+ weil `role` zugleich auf EIGENE_COMMON steht, rettete `mitFremdem`
229
+ eine vorhandene auch nicht herueber - ein Aktualisieren loeschte
230
+ sie also stillschweigend (gefunden 25.08.2026 an
231
+ alias.0.Badezimmer.Rollladen, role „blind"). Ohne gewaehltes
232
+ Muster bleibt die vorhandene stehen. */
233
+ var kanalCommon = { name: altName || teile[teile.length - 1] };
234
+ var altRolle = S.objects[ziel] && S.objects[ziel].common && S.objects[ziel].common.role;
235
+ if (e.want) { kanalCommon.role = e.want; }
236
+ else if (altRolle) { kanalCommon.role = altRolle; }
237
+ raus.push({ id: ziel, neu: !S.objects[ziel], obj: {
238
+ type: (altTyp === 'folder' || altTyp === 'device') ? altTyp : 'channel',
239
+ common: mitFremdem(ziel, kanalCommon),
240
+ native: kanalNative
241
+ } });
242
+
243
+ e.states.forEach(function (s) {
244
+ if (!s.on || !s.n || !s.srcR) { return; }
245
+ var id = ziel + '.' + s.n;
246
+
247
+ var alias = {};
248
+ if (s.srcW && s.srcW !== s.srcR) { alias.id = { read: s.srcR, write: s.srcW }; }
249
+ else { alias.id = s.srcR; }
250
+ if (s.f) { alias.read = s.f; }
251
+ if (s.fw && s.srcW) { alias.write = s.fw; }
252
+
253
+ var common = {
254
+ name: s.caption || s.n,
255
+ role: s.role,
256
+ type: s.typ || 'mixed',
257
+ read: true,
258
+ write: !!s.srcW,
259
+ alias: alias
260
+ };
261
+ if (s.unit) { common.unit = s.unit; }
262
+ /* Ohne Werteliste erkennt der Detektor den fertigen Alias spaeter
263
+ nicht wieder — dieselbe Bedingung wie im Abbild. */
264
+ if (s.states) { common.states = s.states; }
265
+
266
+ var nat = {};
267
+ var altNat = (S.objects[id] && S.objects[id].native)
268
+ ? JSON.parse(JSON.stringify(S.objects[id].native)) : null;
269
+ if (s.nurImAlias && altNat) {
270
+ /* Ein Punkt, den es nur im Alias gibt, behaelt seinen Vermerk —
271
+ sonst schreibt ihn ein Aktualisieren der Vorlage zu, aus der er
272
+ nie kam. */
273
+ nat = altNat;
274
+ } else if (s.manuell) { nat.vonHand = true; }
275
+ else if (e.vorlage) { nat.vorlage = e.vorlage; nat.ausZustand = s.ausVorlage || s.n; }
276
+ else if (altNat) {
277
+ /* Aus dem Aliasmodus heraus gibt es keine Vorlage, und `manuell`
278
+ steht nur beim frischen Hinzufuegen. Ohne diesen Fall schrieb
279
+ ein Aktualisieren `native: {}` ueber jeden Punkt und loeschte
280
+ damit Vorlage, Herkunftsplatz und die Marke `vonHand` — der
281
+ Kanal behielt seine Herkunft, die Punkte verloren sie. */
282
+ nat = altNat;
283
+ }
284
+
285
+ raus.push({ id: id, neu: !S.objects[id],
286
+ obj: { type: 'state', common: mitFremdem(id, common), native: nat } });
287
+ });
288
+
289
+ /* Zum Schluss die Aufzaehlungen. Sie stehen am Ende, weil sie erst
290
+ dann sinnvoll sind, wenn das Ziel feststeht - und weil der
291
+ Trockenlauf sie so unter den Objekten zeigt, zu denen sie gehoeren. */
292
+ enumAenderungen(e).forEach(function (x) { raus.push(x); });
293
+
294
+ return raus;
295
+ }
296
+
297
+ /* Was im Ziel liegt, aber nicht mehr im Entwurf vorkommt.
298
+ „abgewaehlt" = du hast das Haekchen entfernt, also gewollt.
299
+ „nicht im Vorschlag" = existiert nur im Alias, etwa von Hand ergaenzt —
300
+ das wird nicht ungefragt geloescht. */
301
+ export function verwaiste(e) {
302
+ var ziel = e.ziel || e.kanal;
303
+ if (!S.objects[ziel]) { return []; }
304
+ var behalten = {};
305
+ e.states.forEach(function (st) {
306
+ if (st.on && st.n) { behalten[ziel + '.' + st.n] = 1; }
307
+ });
308
+ var imEntwurf = {};
309
+ e.states.forEach(function (st) { if (st.n) { imEntwurf[ziel + '.' + st.n] = 1; } });
310
+
311
+ var pre = ziel + '.';
312
+ return S.keysSorted.filter(function (k) {
313
+ return k.indexOf(pre) === 0 && S.objects[k] && S.objects[k].type === 'state' && !behalten[k];
314
+ }).map(function (k) {
315
+ var abgewaehlt = !!imEntwurf[k];
316
+ return { id: k, grund: abgewaehlt ? tr('write.deselected') : tr('write.notInProposal'), vorgabe: abgewaehlt };
317
+ });
318
+ }
319
+
320
+ /* Was dem Schreiben im Weg steht. */
321
+ export function pruefeSchreiben(e) {
322
+ var probleme = [];
323
+ var an = e.states.filter(function (s) { return s.on; });
324
+ if (!an.length) { probleme.push(tr('write.noneSelected')); }
325
+
326
+ var namen = {};
327
+ an.forEach(function (s) {
328
+ if (!s.n) { probleme.push(tr('write.noName')); return; }
329
+ if (namen[s.n]) { probleme.push(tr('write.duplicateName', s.n)); }
330
+ namen[s.n] = 1;
331
+
332
+ if (!s.srcR) {
333
+ probleme.push(tr('write.noSourceChosen', s.n));
334
+ } else if (!S.objects[s.srcR]) {
335
+ /* Der teuerste Fehler: der js-controller merkt sich „Quelle fehlt"
336
+ dauerhaft und korrigiert das nie. Nur loeschen und neu anlegen
337
+ hilft. Deshalb hier hart sperren. */
338
+ probleme.push(tr('write.sourceMissing', s.n, s.srcR));
339
+ }
340
+
341
+ /* Dasselbe fuer das Schreibziel: die Erkennung darf sich auf einen
342
+ Befehl stuetzen, den das Geraet kennt — geschrieben wird aber erst,
343
+ wenn der Punkt wirklich da ist. Sonst zeigte der Alias auf ein
344
+ Objekt, das es nicht gibt, und der Controller merkt sich auch das
345
+ dauerhaft. */
346
+ if (s.srcW && !S.objects[s.srcW]) {
347
+ probleme.push(tr('write.writeSourceMissing', s.n, s.srcW));
348
+ }
349
+ });
350
+ return probleme;
351
+ }
352
+
353
+ /* Trockenlauf zeigen */
354
+
355
+ /* Welche Entwuerfe geschrieben werden. Bei mehreren Ausgaengen wahlweise
356
+ nur der gezeigte oder alle — die uebrigen kommen frisch aus der Vorlage,
357
+ Aenderungen von Hand gelten nur fuer den gezeigten Ausgang. */
358
+ export function zuSchreiben(alleAusgaenge) {
359
+ /* Je Kanal ein Geraet: fuer jeden steuerbaren Kanal ein eigener
360
+ Entwurf, benannt nach dem Kanal — nicht nach seiner Nummer. Aus
361
+ „Kanal 1" wird `Licht_Bar`, weil das der Name ist, den jemand ihm
362
+ gegeben hat. */
363
+ if (alleAusgaenge && S.entwurf.kanalGeraete && S.entwurf.kanalGeraete.length > 1) {
364
+ /* Ein Ordner fuer das Geraet, darunter je Kanal ein Kanal — dieselbe
365
+ Form, die die Tasmota-Mehrfachvorlage seit jeher erzeugt. */
366
+ var ordner = (S.entwurf.zielOrdner || 'alias.0') + '.' + S.entwurf.zielName;
367
+ var gem = gemeinsameKanaele(S.entwurf.kanal, S.entwurf.kanalGeraete);
368
+ return S.entwurf.kanalGeraete.map(function (k) {
369
+ var v = entwurfFuer(k.id, gem, S.entwurf.gemeinsamMitPlatz);
370
+ if (!v) { return null; }
371
+ v.zielOrdner = ordner;
372
+ v.zielName = k.name;
373
+ v.ziel = ordner + '.' + k.name;
374
+ uebernehmeBestand(v);
375
+ return v;
376
+ }).filter(Boolean);
377
+ }
378
+ if (!alleAusgaenge || !S.entwurf.instanzen || S.entwurf.instanzen.length < 2) { return [S.entwurf]; }
379
+ return S.entwurf.instanzen.map(function (n) {
380
+ if (String(n) === String(S.entwurf.instanz)) { return S.entwurf; }
381
+ var v = vorschlag(S.current, S.entwurf.vorlage, n);
382
+ if (v) {
383
+ v.zielOrdner = S.entwurf.zielOrdner;
384
+ v.zielName = ausgangName(v, n);
385
+ v.ziel = v.zielOrdner + '.' + v.zielName;
386
+ }
387
+ return v;
388
+ }).filter(Boolean);
389
+ }
390
+
391
+ export function zeigeTrockenlauf(alleAusgaenge) {
392
+ if (!S.entwurf) { return; }
393
+ S.trockenAlle = !!alleAusgaenge;
394
+ /* Das Ziel steht sonst erst, wenn zeichneErgebnis gelaufen ist. Wird
395
+ der Trockenlauf aus einem Rueckruf heraus geoeffnet — etwa nachdem
396
+ fehlende Sendepunkte angelegt wurden —, ist der Entwurf frisch und
397
+ hat noch keins. Dann faellt zuSchreiben auf e.kanal zurueck, und das
398
+ ist die Quelle: der Trockenlauf zeigte Objekte unter mqtt-client an. */
399
+ if (S.entwurf) { setzeZiel(S.entwurf); }
400
+
401
+ var entwuerfe = zuSchreiben(S.trockenAlle);
402
+ var liste = [];
403
+ var probleme = [];
404
+ entwuerfe.forEach(function (en) {
405
+ baueObjekte(en).forEach(function (o) {
406
+ if (!liste.some(function (x) { return x.id === o.id; })) { liste.push(o); }
407
+ });
408
+ pruefeSchreiben(en).forEach(function (pr) { probleme.push(pr); });
409
+ });
410
+
411
+ /* Letzte Sicherung, unabhaengig von allem davor: ein Alias gehoert
412
+ unter alias. — sonst schreibt die Werkbank in fremde Zweige. Die
413
+ Pruefung kostet nichts und faengt jeden Weg ab, auf dem das Ziel
414
+ verlorengeht.
415
+
416
+ Eine Ausnahme, und nur diese: Aufzaehlungen. Raum und Funktion stehen
417
+ nicht am Alias, sondern in `enum.rooms.*` und `enum.functions.*` — ohne
418
+ diese Ausnahme liessen sie sich gar nicht setzen. Sie ist eng gefasst:
419
+ nur `enum.`, nur Objekte, die aus `enumAenderungen` stammen und deshalb
420
+ `istEnum` tragen. Alles andere bleibt gesperrt. */
421
+ liste.forEach(function (x) {
422
+ if (x.istEnum) {
423
+ if (x.id.indexOf('enum.rooms.') !== 0 && x.id.indexOf('enum.functions.') !== 0) {
424
+ probleme.push(tr('write.notUnderAlias', x.id));
425
+ }
426
+ return;
427
+ }
428
+ if (x.id.indexOf('alias.') !== 0) {
429
+ probleme.push(tr('write.notUnderAlias', x.id));
430
+ }
431
+ });
432
+ var weg = [];
433
+ entwuerfe.forEach(function (en) {
434
+ verwaiste(en).forEach(function (v) {
435
+ if (!weg.some(function (x) { return x.id === v.id; })) { weg.push(v); }
436
+ });
437
+ });
438
+ S.loeschListe = weg;
439
+ var body = $('#dry-body');
440
+ body.textContent = '';
441
+
442
+ var kopf = el('div');
443
+ kopf.style.marginBottom = '11px';
444
+ kopf.style.fontSize = '12.5px';
445
+ kopf.appendChild(el('b', null, tr('app.objectsCount', liste.length)));
446
+ liste.forEach(function (x) { x.gleich = !x.neu && unveraendert(x.id, x.obj); });
447
+ var neu = liste.filter(function (x) { return x.neu; }).length;
448
+ var gleich = liste.filter(function (x) { return x.gleich; }).length;
449
+ kopf.appendChild(document.createTextNode(' — ' + (gleich
450
+ ? tr('write.summaryTail3', neu, liste.length - neu - gleich, gleich)
451
+ : tr('write.summaryTail', neu, liste.length - neu))));
452
+ /* „uebrig geblieben" hiess in Wahrheit „wird geloescht" — und
453
+ dieselbe rote Zahl stand auch fuer Punkte, die stehenbleiben. Zwei
454
+ gegensaetzliche Faelle unter einer Zahl, ausgerechnet dort, wo
455
+ etwas verschwindet. Jetzt getrennt, und die Zahlen folgen den
456
+ Kaestchen. */
457
+ var wRaus = el('b'), wBleibt = el('span');
458
+ wRaus.style.color = 'var(--bad)';
459
+ wBleibt.style.color = 'var(--ink-3)';
460
+ function zaehlerAuffrischen() {
461
+ var n = S.loeschListe.filter(function (v) { return v.vorgabe; }).length;
462
+ wRaus.textContent = n ? (' — ' + tr('write.willRemove', n)) : '';
463
+ var b = S.loeschListe.length - n;
464
+ wBleibt.textContent = b ? (' — ' + tr('write.willKeep', b)) : '';
465
+ }
466
+ if (weg.length) { kopf.appendChild(wRaus); kopf.appendChild(wBleibt); }
467
+
468
+ /* Der Umschalter fuer die Feldreihenfolge - einmal fuer den ganzen
469
+ Dialog, nicht je Karte: bei acht Objekten legt niemand achtmal um.
470
+ Er erscheint nur, wenn es ueberhaupt etwas zu vergleichen gibt, und
471
+ nur, wenn sich die Reihenfolge irgendwo unterscheidet - sonst
472
+ verspraeche er eine Wirkung, die er nicht hat. */
473
+ var vergleichbar = liste.filter(function (x) { return !x.neu && !x.gleich; });
474
+ var rohAnders = vergleichbar.some(function (x) {
475
+ return objektText(S.objects[x.id], false) !== objektText(S.objects[x.id], true)
476
+ || objektText(x.obj, false) !== objektText(x.obj, true);
477
+ });
478
+ if (vergleichbar.length && rohAnders) {
479
+ var um = el('button', 'btn mini leise', S.trockenRoh
480
+ ? tr('write.diffSorted') : tr('write.diffRaw'));
481
+ um.style.marginLeft = '10px';
482
+ um.title = tr('write.diffSortedHint');
483
+ um.addEventListener('click', function () {
484
+ S.trockenRoh = !S.trockenRoh;
485
+ zeigeTrockenlauf(S.trockenAlle);
486
+ });
487
+ kopf.appendChild(um);
488
+ }
489
+ body.appendChild(kopf);
490
+
491
+ if (weg.length) {
492
+ var wk = el('div', 'card');
493
+ wk.style.marginBottom = '11px';
494
+ wk.style.borderColor = 'var(--bad)';
495
+ var wch = el('div', 'ch');
496
+ wch.appendChild(el('span', 'typ', tr('write.remove')));
497
+ wch.appendChild(el('span', 'chip bad', tr('write.removeCount', weg.length)));
498
+ wch.appendChild(el('span', 'chip mut', tr('write.removeHint')));
499
+ wk.appendChild(wch);
500
+ weg.forEach(function (v, i) {
501
+ var r = el('div', 'slot');
502
+ r.style.gridTemplateColumns = '19px 1fr 150px';
503
+ var cbw = el('span');
504
+ var cb = document.createElement('input');
505
+ cb.type = 'checkbox';
506
+ cb.checked = v.vorgabe;
507
+ cbw.appendChild(cb);
508
+ r.appendChild(cbw);
509
+ r.appendChild(el('span', 'sn', v.id.slice((S.entwurf.ziel || S.entwurf.kanal).length + 1)));
510
+ var g = el('span', 'rx', v.grund);
511
+ if (!v.vorgabe) { g.style.color = 'var(--warn)'; }
512
+ r.appendChild(g);
513
+ cb.addEventListener('change', function () {
514
+ S.loeschListe[i].vorgabe = cb.checked;
515
+ g.style.color = cb.checked ? '' : 'var(--warn)';
516
+ zaehlerAuffrischen();
517
+ });
518
+ wk.appendChild(r);
519
+ });
520
+ body.appendChild(wk);
521
+ zaehlerAuffrischen();
522
+ }
523
+
524
+ if (probleme.length) {
525
+ var pw = el('div', 'aside w');
526
+ pw.style.marginBottom = '11px';
527
+ pw.appendChild(el('b', null, tr('write.cannotWrite')));
528
+ var ul = el('ul');
529
+ ul.style.margin = '6px 0 0';
530
+ ul.style.paddingLeft = '18px';
531
+ probleme.forEach(function (t) { ul.appendChild(el('li', null, t)); });
532
+ pw.appendChild(ul);
533
+
534
+ /* Was fehlt, laesst sich von hier aus anlegen — sonst muesste man
535
+ den Trockenlauf schliessen, in der Karte nachziehen und von vorn
536
+ anfangen. */
537
+ var fehlend = [];
538
+ entwuerfe.forEach(function (en) {
539
+ en.states.forEach(function (s2) {
540
+ if (s2.on && s2.srcW && !S.objects[s2.srcW] && s2.srcW.indexOf('.cmnd.') > -1 &&
541
+ fehlend.indexOf(s2.srcW) === -1) { fehlend.push(s2.srcW); }
542
+ });
543
+ });
544
+ if (fehlend.length) {
545
+ var kb = el('div');
546
+ kb.style.marginTop = '9px';
547
+ var kn2 = el('button', 'btn', tr('write.createSendPoints', fehlend.length));
548
+ kn2.addEventListener('click', function () {
549
+ kn2.disabled = true;
550
+ kn2.textContent = tr('write.writing');
551
+ var offen2 = fehlend.length;
552
+ fehlend.forEach(function (fid) {
553
+ var t2 = fid.split('.cmnd.');
554
+ mqttEinzelnStill(t2[0], t2[1], function () {
555
+ if (--offen2 === 0) {
556
+ /* Erst den Entwurf neu aufbauen lassen, dann den
557
+ Trockenlauf noch einmal rechnen. Sofort geoeffnet zeigte
558
+ er den Rohentwurf, weil die Vorlage noch nicht wieder
559
+ angewandt war. */
560
+ indexNeu();
561
+ nachziehenErledigt();
562
+ zeichneBaum();
563
+ /* Der Neuaufbau wirft das Ziel weg, das im Feld steht —
564
+ danach zeigte der Dialog Objekte fuer einen anderen
565
+ Ordner an als eingegeben, und „Jetzt anlegen" haette
566
+ sie dort auch angelegt. Also merken und zurueckgeben. */
567
+ var altZiel = S.entwurf
568
+ ? { o: S.entwurf.zielOrdner, n: S.entwurf.zielName } : null;
569
+ if (S.current) {
570
+ waehle(S.current, function () {
571
+ if (altZiel && S.entwurf) {
572
+ S.entwurf.zielOrdner = altZiel.o;
573
+ S.entwurf.zielName = altZiel.n;
574
+ S.entwurf.angefasst = true;
575
+ zeichneErgebnis();
576
+ }
577
+ zeigeTrockenlauf(S.trockenAlle);
578
+ });
579
+ } else { zeigeTrockenlauf(S.trockenAlle); }
580
+ }
581
+ });
582
+ });
583
+ });
584
+ kb.appendChild(kn2);
585
+ pw.appendChild(kb);
586
+ }
587
+ body.appendChild(pw);
588
+ }
589
+
590
+ /* Je Objekt eine Zeile, das JSON erst beim Aufklappen.
591
+
592
+ Vorher stand unter jedem Eintrag der ganze JSON-Block. Bei zehn
593
+ Objekten waren das ueber zweihundert Zeilen, durch die man scrollen
594
+ musste, um zu sehen, was ueberhaupt entsteht — und die Frage, die
595
+ der Trockenlauf beantworten soll, ist zuerst „was wird angelegt und
596
+ woher liest es", nicht „wie sieht das Objekt innen aus".
597
+
598
+ details/summary statt eigenem Klick-Handler: das klappt von selbst,
599
+ laesst sich mit der Tastatur bedienen und braucht keinen Zustand,
600
+ der ueber das Neuzeichnen gerettet werden muesste. */
601
+ liste.forEach(function (x) {
602
+ var k = el('details', 'card zeile');
603
+ k.style.marginBottom = '5px';
604
+
605
+ var ch = document.createElement('summary');
606
+ ch.className = 'ch';
607
+ ch.appendChild(el('span',
608
+ 'chip ' + (x.neu ? 'ok' : (x.gleich ? 'mut' : 'warn')),
609
+ x.neu ? tr('write.new') : (x.gleich ? tr('write.unchanged') : tr('write.changes'))));
610
+ ch.appendChild(el('span', 'chip mut', x.obj.type));
611
+
612
+ /* Ziel und Quelle untereinander, beide linksbuendig und beschriftet.
613
+ Nebeneinander in eine Zeile passte beides fast nie: allein die
614
+ Ziel-ID ist bei einem Homematic-Geraet mit Namen und Seriennummer
615
+ schon ueber sechzig Zeichen lang, und die Quelle rutschte dann
616
+ rechts an den Rand, wo sie niemand mehr las. */
617
+ var paar = el('span', 'zpaar');
618
+ paar.appendChild(el('span', 'zlabel', tr('write.rowTarget')));
619
+ paar.appendChild(el('span', 'zid', x.id));
620
+
621
+ /* Bei einer Aufzaehlung ist die Auskunft eine andere: nicht „woher
622
+ liest der Punkt", sondern „was passiert mit dieser Liste". */
623
+ if (x.istEnum) {
624
+ var was = x.dazu ? tr('enums.addTo') : (x.weg ? tr('enums.removeFrom') : '');
625
+ if (was) {
626
+ paar.appendChild(el('span', 'zlabel', ''));
627
+ paar.appendChild(el('span', 'zq', was));
628
+ }
629
+ /* Ein nachgetragenes Bild ist eine Aenderung an einem fremden
630
+ Objekt. Sie gehoert benannt, nicht nur ins aufgeklappte JSON. */
631
+ if (x.ikonDazu) {
632
+ paar.appendChild(el('span', 'zlabel', ''));
633
+ paar.appendChild(el('span', 'zq', tr('enums.iconAdded')));
634
+ }
635
+ }
636
+
637
+ var al = x.obj.common && x.obj.common.alias;
638
+ if (al) {
639
+ var q = (typeof al.id === 'string') ? al.id : (al.id && (al.id.read || al.id.write)) || '';
640
+ /* Liest und schreibt der Punkt an verschiedenen Stellen, steht
641
+ hier die Lesequelle; die ganze Wahrheit zeigt das JSON. */
642
+ var beides = (al.id && typeof al.id === 'object' && al.id.read && al.id.write
643
+ && al.id.read !== al.id.write);
644
+ if (q) {
645
+ paar.appendChild(el('span', 'zlabel', tr('write.rowSource')));
646
+ paar.appendChild(el('span', 'zq', q + (beides ? ' \u2026' : '')));
647
+ }
648
+ }
649
+ ch.appendChild(paar);
650
+ k.appendChild(ch);
651
+
652
+ /* Aendert sich etwas, stehen Bestand und Entwurf nebeneinander -
653
+ Zeile gegen Zeile, wie in einem Vergleichswerkzeug. Sonst genuegt
654
+ das eine JSON (bei „neu" gibt es keinen Vorher-Stand, bei
655
+ „unveraendert" nichts zu vergleichen). */
656
+ if (!x.neu && !x.gleich) {
657
+ var vgl = el('div', 'dvgl');
658
+ vgl.appendChild(el('span', 'dkopf', tr('write.diffOld')));
659
+ vgl.appendChild(el('span', 'dkopf', tr('write.diffNew')));
660
+ zeilenVergleich(objektText(S.objects[x.id], !S.trockenRoh),
661
+ objektText(x.obj, !S.trockenRoh))
662
+ .forEach(function (z) {
663
+ vgl.appendChild(el('span', 'dz l ' + z.art, z.l === undefined ? '' : z.l));
664
+ vgl.appendChild(el('span', 'dz r ' + z.art, z.r === undefined ? '' : z.r));
665
+ });
666
+ k.appendChild(vgl);
667
+ } else {
668
+ var pre = el('div', 'raw');
669
+ pre.style.margin = '2px 12px 10px';
670
+ pre.style.whiteSpace = 'pre';
671
+ pre.style.maxHeight = 'none';
672
+ pre.textContent = JSON.stringify(x.obj, null, 2);
673
+ k.appendChild(pre);
674
+ }
675
+ body.appendChild(k);
676
+ });
677
+
678
+ var zt = $('#dry-titel');
679
+ if (zt) {
680
+ var zielName = S.entwurf.ziel || S.entwurf.kanal;
681
+ zt.textContent = (S.objects[zielName] ? tr('write.dryTitleUpdate') : tr('write.dryTitleCreate')) +
682
+ (entwuerfe.length > 1 ? tr('write.dryOutputs', entwuerfe.length) : '');
683
+ }
684
+ /* Beim Oeffnen wieder herstellen: der Dialog wird wiederverwendet, und
685
+ nach einem erfolgreichen Durchgang ist der Knopf ausgeblendet. */
686
+ $('#btn-dry-write').hidden = false;
687
+ $('#btn-dry-write').disabled = probleme.length > 0;
688
+ $('#btn-dry-write').textContent = S.objects[S.entwurf.ziel || S.entwurf.kanal] ? tr('write.nowUpdate') : tr('write.now');
689
+ $('#dlg-dry').showModal();
690
+ }
691
+
692
+ /* Einen Alias wieder loswerden.
693
+
694
+ Anlegen und Aktualisieren gab es, Entfernen nicht — und der
695
+ naheliegende Umweg lief ins Leere: haekelt man alle Punkte ab, listet
696
+ der Trockenlauf sie zwar auf, sperrt aber das Schreiben mit „kein
697
+ einziger Datenpunkt ausgewaehlt". Ein einmal angelegter Alias liess
698
+ sich nur ueber die Objektverwaltung entfernen. */
699
+
700
+ /* Die Geschwister eines Ausgangs: bei einem Mehrfachgeraet tragen alle
701
+ Kanaele dieselbe Quelle. Ohne sie loescht „Alias entfernen" nur einen
702
+ von vier Ausgaengen, und der Rest bleibt verwaist stehen — man glaubt,
703
+ das Geraet sei weg. */
704
+ function geschwisterVon(ziel) {
705
+ var o = S.objects[ziel];
706
+ var q = o && o.native && o.native.quelle;
707
+ if (!q) { return []; }
708
+ return S.keysSorted.filter(function (id) {
709
+ if (id === ziel || id.indexOf('alias.') !== 0) { return false; }
710
+ var x = S.objects[id];
711
+ return x && x.type === 'channel' && x.native && x.native.quelle === q;
712
+ });
713
+ }
714
+
715
+ export function zeigeLoeschen() {
716
+ var ziel = (S.entwurf && (S.entwurf.ziel || S.entwurf.kanal)) || S.current;
717
+ if (!ziel || ziel.indexOf('alias.') !== 0 || !S.objects[ziel]) { return; }
718
+ S.loeschZiel = ziel;
719
+ S.loeschAlleAusgaenge = false;
720
+ zeichneLoeschen();
721
+ }
722
+
723
+ function loeschUmfang() {
724
+ var ziel = S.loeschZiel;
725
+ var wurzeln = [ziel];
726
+ if (S.loeschAlleAusgaenge) { wurzeln = wurzeln.concat(geschwisterVon(ziel)); }
727
+ var alle = [];
728
+ wurzeln.forEach(function (w) {
729
+ alle.push(w);
730
+ S.keysSorted.forEach(function (id) {
731
+ if (id.indexOf(w + '.') === 0 && alle.indexOf(id) === -1) { alle.push(id); }
732
+ });
733
+ });
734
+ return alle;
735
+ }
736
+
737
+ /* Welche selbst angelegten Aufzaehlungen wuerden durch dieses Loeschen
738
+ leer? Ein Ergebnis fuer Dialog und Tat — der Dialog nennt genau das,
739
+ was hinterher geschieht, keine stille Zugabe. Gerechnet gegen die
740
+ Loeschliste, nicht gegen den Speicherstand: ob der Admin seine
741
+ Mitglieder-Austragung schon gemeldet hat, ist Zufall. */
742
+ function enumsDieLeerWuerden(alle) {
743
+ var raus = [];
744
+ ['rooms', 'functions'].forEach(function (art) {
745
+ var betroffen = {};
746
+ alle.forEach(function (id) {
747
+ enumsVon(id, art).forEach(function (eid) { betroffen[eid] = true; });
748
+ });
749
+ Object.keys(betroffen).forEach(function (eid) {
750
+ var o = enums[eid];
751
+ if (!o || !o.native || o.native.angelegtVon !== 'alias-workbench') { return; }
752
+ var uebrig = ((o.common && o.common.members) || []).filter(function (m) {
753
+ return alle.indexOf(m) === -1;
754
+ });
755
+ if (!uebrig.length) {
756
+ raus.push({ id: eid, art: art,
757
+ name: txt((o.common || {}).name) || eid.split('.').pop() });
758
+ }
759
+ });
760
+ });
761
+ return raus;
762
+ }
763
+
764
+ export function zeichneLoeschen() {
765
+ var ziel = S.loeschZiel;
766
+ var body = $('#del-body');
767
+ body.textContent = '';
768
+ var alle = loeschUmfang();
769
+
770
+ var kopf = el('div');
771
+ kopf.style.marginBottom = '11px';
772
+ kopf.style.fontSize = '12.5px';
773
+ kopf.appendChild(el('b', null, tr('del.count', alle.length)));
774
+ body.appendChild(kopf);
775
+
776
+ var hin = el('div', 'aside w');
777
+ hin.style.marginBottom = '11px';
778
+ hin.textContent = tr('del.hint');
779
+ body.appendChild(hin);
780
+
781
+ var gesch = geschwisterVon(ziel);
782
+ if (gesch.length) {
783
+ var gw = el('div', 'aside');
784
+ gw.style.marginBottom = '11px';
785
+ var lab = document.createElement('label');
786
+ lab.style.display = 'flex';
787
+ lab.style.gap = '8px';
788
+ lab.style.alignItems = 'flex-start';
789
+ lab.style.cursor = 'pointer';
790
+ var cb = document.createElement('input');
791
+ cb.type = 'checkbox';
792
+ cb.checked = S.loeschAlleAusgaenge;
793
+ cb.addEventListener('change', function () {
794
+ S.loeschAlleAusgaenge = cb.checked;
795
+ zeichneLoeschen();
796
+ });
797
+ lab.appendChild(cb);
798
+ var txt2 = el('span');
799
+ txt2.appendChild(el('b', null, tr('del.allOutputs', gesch.length + 1)));
800
+ txt2.appendChild(el('div', 'hint', tr('del.allOutputsHint',
801
+ gesch.map(function (x) { return x.split('.').pop(); }).join(', '))));
802
+ lab.appendChild(txt2);
803
+ gw.appendChild(lab);
804
+ body.appendChild(gw);
805
+ }
806
+
807
+ var k = el('div', 'card');
808
+ var ch = el('div', 'ch');
809
+ ch.appendChild(el('span', 'typ', tr('del.objects')));
810
+ ch.appendChild(el('span', 'chip bad', String(alle.length)));
811
+ k.appendChild(ch);
812
+ alle.forEach(function (id) {
813
+ var r = el('div', 'slot');
814
+ r.style.gridTemplateColumns = '1fr 90px';
815
+ r.appendChild(el('span', 'sn', id));
816
+ r.appendChild(el('span', 'rx', (S.objects[id] && S.objects[id].type) || ''));
817
+ k.appendChild(r);
818
+ });
819
+ body.appendChild(k);
820
+
821
+ /* Mitangelegte Aufzaehlungen, die dadurch leer wuerden, gehen mit —
822
+ und stehen deshalb hier, nicht nur im Nachhinein im Objektbaum. */
823
+ var mitEnums = enumsDieLeerWuerden(alle);
824
+ if (mitEnums.length) {
825
+ var ew = el('div', 'aside');
826
+ ew.style.marginTop = '11px';
827
+ mitEnums.forEach(function (x) {
828
+ ew.appendChild(el('div', null,
829
+ tr(x.art === 'rooms' ? 'del.alsoRoom' : 'del.alsoFunction', x.name)));
830
+ });
831
+ body.appendChild(ew);
832
+ }
833
+
834
+ var b = $('#btn-del-go');
835
+ b.disabled = false;
836
+ b.hidden = false;
837
+ b.textContent = tr('del.now');
838
+ if (!$('#dlg-del').open) { $('#dlg-del').showModal(); }
839
+ }
840
+
841
+ export function loescheAlias() {
842
+ if (!S.loeschZiel) { return; }
843
+ var ziel = S.loeschZiel;
844
+ var alle = loeschUmfang();
845
+ /* Kinder vor den Eltern, sonst bleiben Waisen stehen. */
846
+ alle.sort(function (a, b) { return b.length - a.length; });
847
+
848
+ /* Vor dem Loeschen bestimmt — danach steht die Mitgliedschaft
849
+ nirgends mehr (die traegt der Admin beim delObject selbst aus).
850
+ Dieselbe Rechnung hat der Dialog gezeigt; hier wird getan, was
851
+ angekuendigt war. */
852
+ var enumsMitzunehmen = enumsDieLeerWuerden(alle).map(function (x) { return x.id; });
853
+
854
+ var b = $('#btn-del-go');
855
+ b.disabled = true;
856
+ b.textContent = tr('write.writing');
857
+
858
+ var offen = alle.length, fehler = [];
859
+ alle.forEach(function (id) {
860
+ socket.emit('delObject', id, function (err) {
861
+ if (err) { fehler.push(id + ': ' + err); }
862
+ if (--offen === 0) {
863
+ var body = $('#del-body');
864
+ body.textContent = '';
865
+ var m = el('div');
866
+ m.style.fontSize = '13px';
867
+ if (fehler.length) {
868
+ m.appendChild(el('b', null, tr('write.failed', fehler.length)));
869
+ } else {
870
+ m.appendChild(el('b', null, tr('del.done', alle.length)));
871
+ }
872
+ body.appendChild(m);
873
+ alle.forEach(function (x) { delete S.objects[x]; });
874
+ S.keysSorted = Object.keys(S.objects).sort();
875
+
876
+ /* Ordner, die dadurch leer geworden sind, mitnehmen — aber nur
877
+ die eigenen Zwischenebenen unterhalb von alias.0, und nur
878
+ solange wirklich nichts mehr darin liegt. Und Aufzaehlungen,
879
+ die die Werkbank selbst angelegt hat und die jetzt leer sind,
880
+ gleich mit. */
881
+ /* Nach dem Erfolg gibt es nichts mehr zu entfernen - der Knopf
882
+ verschwindet, statt ausgegraut neben „Schliessen" zu stehen.
883
+ Nach einem Fehlschlag bleibt er als „Noch einmal". */
884
+ if (fehler.length) {
885
+ b.disabled = false;
886
+ b.textContent = tr('write.retry');
887
+ } else {
888
+ b.hidden = true;
889
+ }
890
+ leereEnumsRaeumen(enumsMitzunehmen, alle, function () {
891
+ leereOrdnerRaeumen(ziel, function () {
892
+ S.loeschZiel = null;
893
+ S.entwurf = null;
894
+ S.current = null;
895
+ zeichneBaum();
896
+ zeichneErgebnis();
897
+ });
898
+ });
899
+ }
900
+ });
901
+ });
902
+ }
903
+
904
+ /* Aufzaehlungen, die die Werkbank selbst angelegt hat (Herkunftsmarke in
905
+ native) und deren letztes Mitglied gerade geloescht wurde, verschwinden
906
+ mit. Fremde und vorbestehende Aufzaehlungen bleiben — dort ist leer
907
+ kein Fehler, sondern ein Zustand, den jemand anders verantwortet.
908
+ Gerechnet wird gegen die eigene Loeschliste, nicht gegen den Stand im
909
+ Speicher: der Admin traegt Mitgliedschaften beim delObject selbst aus,
910
+ und ob seine Meldung schon angekommen ist, ist Zufall. */
911
+ function leereEnumsRaeumen(eids, geloescht, fertig) {
912
+ var offen = eids.length;
913
+ if (!offen) { return fertig(); }
914
+ eids.forEach(function (eid) {
915
+ var o = enums[eid];
916
+ var uebrig = ((o && o.common && o.common.members) || []).filter(function (m) {
917
+ return geloescht.indexOf(m) === -1;
918
+ });
919
+ var eigene = !!(o && o.native && o.native.angelegtVon === 'alias-workbench');
920
+ if (!o || uebrig.length || !eigene) {
921
+ if (--offen === 0) { fertig(); }
922
+ return;
923
+ }
924
+ socket.emit('delObject', eid, function () {
925
+ delete enums[eid];
926
+ if (--offen === 0) { fertig(); }
927
+ });
928
+ });
929
+ }
930
+
931
+ /* ================== Einen Alias verlegen ==================
932
+
933
+ Verschieben und Umbenennen sind im Objektspeicher dasselbe, und beides
934
+ gibt es dort nicht: Eine Kennung laesst sich nicht aendern. Was hier
935
+ „verlegen“ heisst, ist immer anlegen unter der neuen Kennung und
936
+ loeschen unter der alten. Ob sich dabei der Ordner aendert, der Name
937
+ oder beides, ist nur eine Frage der Felder.
938
+
939
+ Das Unangenehme daran ist nicht der Umzug, sondern was zurueckbleibt.
940
+ Eine Messreihe in InfluxDB haengt an der Kennung, nicht am Geraet: Die
941
+ Aufzeichnung wandert mit, die bisherigen Werte bleiben unter der alten
942
+ Kennung liegen und werden nicht mehr ergaenzt. Dasselbe gilt fuer
943
+ jedes Skript und jede Ansicht, die die alte Kennung nennt. Die
944
+ Werkbank kann das nicht reparieren - sie kann es aber benennen, und
945
+ genau das tut der Dialog, bevor irgendetwas geschieht. */
946
+
947
+
948
+ function verlegeUmfang(alt) {
949
+ return S.keysSorted.filter(function (id) {
950
+ return id === alt || id.indexOf(alt + '.') === 0;
951
+ });
952
+ }
953
+
954
+ /* Wer nennt diese Kennung sonst noch?
955
+
956
+ Die Skripte des javascript-Adapters lassen sich durchsuchen; sie sind
957
+ der haeufigste Ort, an dem eine Kennung im Klartext steht. vis-
958
+ Ansichten und fremde Adapter kann die Werkbank nicht durchsehen -
959
+ darauf weist der Dialog eigens hin, statt Vollstaendigkeit
960
+ vorzutaeuschen. */
961
+ export function sucheInSkripten(kennung, fertig) {
962
+ var treffer = [];
963
+ var fertigMit = function () { fertig(treffer); };
964
+ try {
965
+ socket.emit('getObjectView', 'script', 'javascript',
966
+ { startkey: 'script.js.', endkey: 'script.js.\u9999' }, function (err, doc) {
967
+ if (err || !doc || !doc.rows) { return fertigMit(); }
968
+ doc.rows.forEach(function (r) {
969
+ var o = r.value;
970
+ var q = o && o.common && o.common.source;
971
+ if (!q || q.indexOf(kennung) === -1) { return; }
972
+ var zeilen = String(q).split('\n');
973
+ var stellen = [];
974
+ zeilen.forEach(function (z, i) {
975
+ if (z.indexOf(kennung) > -1) { stellen.push(i + 1); }
976
+ });
977
+ treffer.push({
978
+ id: r.id,
979
+ name: txt((o.common || {}).name) || r.id.split('.').pop(),
980
+ an: !!(o.common && o.common.enabled),
981
+ zeilen: stellen
982
+ });
983
+ });
984
+ fertigMit();
985
+ });
986
+ } catch { fertigMit(); }
987
+ }
988
+
989
+ export function zeigeVerlegen() {
990
+ /* Der Knopf steht auch an der Quelle, sobald es den Alias gibt (T1).
991
+ Dort ist S.current die Quelle, nicht der Alias - gemeint ist immer
992
+ das Ziel des Entwurfs. Genauso haelt es zeigeLoeschen; zeigeTausch
993
+ tat es bis 05.09.2026 nicht, und beide Knoepfe waren an der Quelle
994
+ sichtbar, aber wirkungslos: kein Dialog, keine Meldung. */
995
+ var alt = (S.entwurf && (S.entwurf.ziel || S.entwurf.kanal)) || S.current;
996
+ if (!alt || alt.indexOf('alias.') !== 0 || !S.objects[alt]) { return; }
997
+ S.verlegeZiel = alt;
998
+
999
+ var teile = alt.split('.');
1000
+ var name0 = teile.pop();
1001
+ var ordner0 = teile.join('.');
1002
+
1003
+ var body = $('#move-body');
1004
+ body.textContent = '';
1005
+
1006
+ /* --- die beiden Felder --- */
1007
+ var bar = el('div', 'zielbar');
1008
+ bar.appendChild(el('span', 'zl', tr('target.folder')));
1009
+
1010
+ /* Dasselbe Feld wie beim Anlegen, mit derselben Vorschlagsliste. Ohne
1011
+ sie muesste man den Zielordner auswendig wissen und fehlerfrei
1012
+ tippen - bei einem Vorgang, der Objekte loescht, die falscheste
1013
+ Stelle fuer einen Tippfehler. */
1014
+ var wrapO = el('div', 'feldwrap');
1015
+ var iO = el('input', 'tx ordnerfeld');
1016
+ iO.type = 'text';
1017
+ iO.setAttribute('autocomplete', 'off');
1018
+ iO.value = ordner0.indexOf('alias.0.') === 0 ? ordner0.slice('alias.0.'.length) : '';
1019
+ iO.placeholder = tr('target.folderPlaceholder');
1020
+ wrapO.appendChild(iO);
1021
+
1022
+ /* Kreuz zum Leeren - dann liegt der Alias direkt unter alias.0. */
1023
+ var xO = el('span', 'feldx');
1024
+ xO.title = tr('target.clearField');
1025
+ xO.setAttribute('aria-label', tr('target.clearField'));
1026
+ xO.addEventListener('mousedown', function (ev) {
1027
+ ev.preventDefault();
1028
+ iO.value = '';
1029
+ malen();
1030
+ ordnerMalen(true);
1031
+ iO.focus();
1032
+ });
1033
+ wrapO.appendChild(xO);
1034
+
1035
+ var listeO = el('div', 'vorschlaege');
1036
+ listeO.hidden = true;
1037
+ wrapO.appendChild(listeO);
1038
+ bar.appendChild(wrapO);
1039
+
1040
+ /* Beim Aufklappen die ganze Liste, erst beim Tippen filtern.
1041
+
1042
+ Vorher filterte auch das Aufklappen mit dem Feldinhalt - und der ist
1043
+ vorbelegt. Wer den Alias nur umbenennen wollte, sah genau einen
1044
+ Eintrag: den Ordner, in dem er ohnehin liegt. Um irgendwohin sonst
1045
+ zu verschieben, musste man das Feld erst leeren. Dasselbe hatte das
1046
+ Musterfeld schon gelernt: aufklappen zeigt alles, tippen sucht. */
1047
+ var ordnerMalen = function (alleZeigen) {
1048
+ var such = alleZeigen ? '' : String(iO.value || '').trim().toLowerCase();
1049
+ var jetzt = String(iO.value || '').trim();
1050
+ var alle = ordnerUnterAlias()
1051
+ .map(function (o) { return o.slice('alias.0.'.length); })
1052
+ .filter(Boolean)
1053
+ .filter(function (o) { return !such || o.toLowerCase().indexOf(such) > -1; });
1054
+
1055
+ listeO.textContent = '';
1056
+
1057
+ /* „kein Ordner“ gehoert dazu: alias.0 selbst ist ein gueltiger Platz,
1058
+ steht aber in keiner Ordnerliste. */
1059
+ if (!such && jetzt) {
1060
+ var z0 = el('div', 'vz');
1061
+ z0.appendChild(el('span', null, tr('move.noFolder')));
1062
+ z0.addEventListener('mousedown', function (ev) {
1063
+ ev.preventDefault();
1064
+ iO.value = '';
1065
+ listeO.hidden = true;
1066
+ malen();
1067
+ });
1068
+ listeO.appendChild(z0);
1069
+ }
1070
+
1071
+ alle.forEach(function (o) {
1072
+ var z = el('div', 'vz' + (o === jetzt ? ' gewaehlt' : ''));
1073
+ z.appendChild(el('span', null, o));
1074
+ z.addEventListener('mousedown', function (ev) {
1075
+ ev.preventDefault();
1076
+ iO.value = o;
1077
+ listeO.hidden = true;
1078
+ malen();
1079
+ });
1080
+ listeO.appendChild(z);
1081
+ });
1082
+
1083
+ listeO.hidden = !listeO.childNodes.length;
1084
+ };
1085
+ iO.addEventListener('focus', function () { ordnerMalen(true); });
1086
+ iO.addEventListener('blur', function () {
1087
+ setTimeout(function () { listeO.hidden = true; }, 140);
1088
+ });
1089
+ bar.appendChild(el('span', 'zl', tr('target.name')));
1090
+ var iN = el('input', 'tx');
1091
+ iN.type = 'text';
1092
+ iN.value = name0;
1093
+ iN.style.width = '200px';
1094
+ bar.appendChild(iN);
1095
+ body.appendChild(bar);
1096
+
1097
+ var vorschau = el('div');
1098
+ body.appendChild(vorschau);
1099
+
1100
+ var saeubern = function (t) {
1101
+ return String(t).trim()
1102
+ .replace(/[^\w.\- \u00e4\u00f6\u00fc\u00c4\u00d6\u00dc\u00df]/g, '_')
1103
+ .replace(/\s+/g, '_')
1104
+ .replace(/\.{2,}/g, '.')
1105
+ .replace(/^\.+|\.+$/g, '');
1106
+ };
1107
+ var neuesZiel = function () {
1108
+ /* Zwei verschiedene Regeln, und das mit Absicht: im ORDNER trennen
1109
+ Punkte die Unterebenen und muessen bleiben, im NAMEN wuerden sie
1110
+ eine neue Ebene aufmachen. Der Name geht deshalb durch dieselbe
1111
+ Funktion wie der automatische Vorschlag - vorher hatte dieser
1112
+ Dialog seine eigene, und aus „Dimmer Neu, Test." wurde
1113
+ `Dimmer_Neu__Test` statt `Dimmer_Neu_Test` (26.08.2026). */
1114
+ var o = saeubern(iO.value), n = kennungtauglich(iN.value);
1115
+ if (!n) { return ''; }
1116
+ return 'alias.0.' + (o ? o + '.' : '') + n;
1117
+ };
1118
+
1119
+ var malen = function () {
1120
+ vorschau.textContent = '';
1121
+ var neu = neuesZiel();
1122
+ var b = $('#btn-move-go');
1123
+
1124
+ if (!neu) {
1125
+ vorschau.appendChild(el('div', 'aside w', tr('move.needName')));
1126
+ b.disabled = true;
1127
+ return;
1128
+ }
1129
+ if (neu === alt) {
1130
+ vorschau.appendChild(el('div', 'aside', tr('move.unchanged')));
1131
+ b.disabled = true;
1132
+ return;
1133
+ }
1134
+ if (S.objects[neu]) {
1135
+ vorschau.appendChild(el('div', 'aside w', tr('move.exists', neu)));
1136
+ b.disabled = true;
1137
+ return;
1138
+ }
1139
+ b.disabled = false;
1140
+
1141
+ var umfang = verlegeUmfang(alt);
1142
+
1143
+ /* Quelle und Ziel einmal, nicht je Datenpunkt.
1144
+
1145
+ Hier stand zuerst jedes Objekt mit voller alter und neuer Kennung
1146
+ untereinander - acht Zeilen, in denen sich immer nur die ersten
1147
+ beiden Glieder unterschieden und die Punktnamen wortgleich
1148
+ wiederholt wurden. Was sich aendert, ist der Pfad; was gleich
1149
+ bleibt, gehoert in eine Zeile. */
1150
+ var k = el('div', 'card');
1151
+ var ch = el('div', 'ch');
1152
+ ch.appendChild(el('span', 'typ', tr('move.willMove')));
1153
+ ch.appendChild(el('span', 'chip ok', String(umfang.length)));
1154
+ k.appendChild(ch);
1155
+
1156
+ var vz = el('div', 'slot');
1157
+ vz.style.gridTemplateColumns = '52px 1fr';
1158
+ vz.appendChild(el('span', 'rx', tr('move.from')));
1159
+ vz.appendChild(el('span', 'sn', alt));
1160
+ k.appendChild(vz);
1161
+
1162
+ var nz = el('div', 'slot');
1163
+ nz.style.gridTemplateColumns = '52px 1fr';
1164
+ nz.appendChild(el('span', 'rx', tr('move.to')));
1165
+ nz.appendChild(el('span', 'sn', neu));
1166
+ k.appendChild(nz);
1167
+
1168
+ var punkte = umfang.filter(function (id) { return id !== alt; })
1169
+ .map(function (id) { return id.slice(alt.length + 1); });
1170
+ if (punkte.length) {
1171
+ var pz = el('div', 'slot');
1172
+ pz.style.gridTemplateColumns = '52px 1fr';
1173
+ pz.appendChild(el('span', 'rx', tr('move.with')));
1174
+ pz.appendChild(el('span', 'sn', punkte.join(', ')));
1175
+ k.appendChild(pz);
1176
+ }
1177
+ /* Fehlende Ordner ueber dem Ziel werden mit angelegt - das steht
1178
+ hier, weil nichts still geschehen soll (verlegeAlias tut es). */
1179
+ var teileV = neu.split('.'), ordnerNeu = [];
1180
+ for (var iV = 3; iV < teileV.length; iV++) {
1181
+ var pfadV = teileV.slice(0, iV).join('.');
1182
+ if (!S.objects[pfadV]) { ordnerNeu.push(pfadV); }
1183
+ }
1184
+ if (ordnerNeu.length) {
1185
+ var oz = el('div', 'slot');
1186
+ oz.style.gridTemplateColumns = '52px 1fr';
1187
+ oz.appendChild(el('span', 'rx', tr('move.newFolder')));
1188
+ oz.appendChild(el('span', 'sn', ordnerNeu.join(', ')));
1189
+ k.appendChild(oz);
1190
+ }
1191
+ vorschau.appendChild(k);
1192
+
1193
+ /* --- Warnungen --- */
1194
+ var mitCustom = umfang.filter(function (id) {
1195
+ var c = S.objects[id] && S.objects[id].common;
1196
+ return c && c.custom && Object.keys(c.custom).length;
1197
+ });
1198
+ if (mitCustom.length) {
1199
+ var w1 = el('div', 'aside w');
1200
+ w1.appendChild(el('b', null, tr('move.warnHistoryHead')));
1201
+ w1.appendChild(document.createTextNode(' ' + tr('move.warnHistory')));
1202
+ var ul = el('ul');
1203
+ mitCustom.forEach(function (id) {
1204
+ var adapter = Object.keys(S.objects[id].common.custom).join(', ');
1205
+ ul.appendChild(el('li', null, id + ' \u2192 ' + adapter));
1206
+ });
1207
+ w1.appendChild(ul);
1208
+ vorschau.appendChild(w1);
1209
+ }
1210
+
1211
+ var w2 = el('div', 'aside');
1212
+ w2.appendChild(el('b', null, tr('move.warnRefsHead')));
1213
+ w2.appendChild(document.createTextNode(' ' + tr('move.warnRefs')));
1214
+ vorschau.appendChild(w2);
1215
+
1216
+ /* Skripte erst suchen, wenn das Ziel steht - sonst sucht sie bei
1217
+ jedem Tastendruck. */
1218
+ var skriptFeld = el('div');
1219
+ vorschau.appendChild(skriptFeld);
1220
+ sucheInSkripten(alt, function (treffer) {
1221
+ if (neuesZiel() !== neu) { return; } /* inzwischen weitergetippt */
1222
+ skriptFeld.textContent = '';
1223
+ if (!treffer.length) {
1224
+ skriptFeld.appendChild(el('div', 'aside', tr('move.noScripts')));
1225
+ return;
1226
+ }
1227
+ var w3 = el('div', 'aside w');
1228
+ /* Ein Skript ist kein „1 Skripte“. */
1229
+ w3.appendChild(el('b', null, treffer.length === 1
1230
+ ? tr('move.scriptsHead1')
1231
+ : tr('move.scriptsHead', treffer.length)));
1232
+ var ul3 = el('ul');
1233
+ treffer.forEach(function (t) {
1234
+ ul3.appendChild(el('li', null, t.name + (t.an ? '' : ' (' + tr('move.scriptOff') + ')') +
1235
+ ' \u2014 ' + (t.zeilen.length === 1
1236
+ ? tr('move.scriptLines', t.zeilen[0])
1237
+ : tr('move.scriptLinesN', t.zeilen.join(', ')))));
1238
+ });
1239
+ w3.appendChild(ul3);
1240
+ skriptFeld.appendChild(w3);
1241
+ });
1242
+ };
1243
+
1244
+ /* Den Knopf zuruecksetzen, BEVOR `malen` urteilt.
1245
+
1246
+ Stand das Zuruecksetzen dahinter, hob es das Urteil gleich wieder
1247
+ auf: beim Oeffnen steht „nichts zu tun“ da, und der Knopf war
1248
+ trotzdem freigegeben. Gedrueckt hat er nichts getan - `verlegeAlias`
1249
+ faengt den Fall ab -, aber ein Knopf, der nichts tut, gehoert nicht
1250
+ freigegeben. */
1251
+ var b0 = $('#btn-move-go');
1252
+ b0.hidden = false;
1253
+ b0.disabled = false;
1254
+ b0.textContent = tr('move.now');
1255
+
1256
+ iO.addEventListener('input', function () { ordnerMalen(false); malen(); });
1257
+ iN.addEventListener('input', malen);
1258
+ /* Beim Verlassen steht im Feld, was auch geschrieben wird. Die
1259
+ Zielleiste haelt es seit dem 26.08.2026 so; hier blieb „Dimmer Neu,
1260
+ Test." stehen, waehrend die Vorschau darunter schon
1261
+ `Dimmer_Neu_Test` zeigte - dieselbe Lage wie D19, nur umgekehrt
1262
+ herum: das Ziel stimmte, das Feld log. Gefunden 05.09.2026 (T19). */
1263
+ iN.addEventListener('change', function () {
1264
+ var sauber = kennungtauglich(iN.value);
1265
+ if (sauber && sauber !== iN.value) { iN.value = sauber; malen(); }
1266
+ });
1267
+ malen();
1268
+ if (!$('#dlg-move').open) { $('#dlg-move').showModal(); }
1269
+ setTimeout(function () { iN.focus(); iN.select(); }, 0);
1270
+
1271
+ /* Der Knopf braucht das Ziel beim Druecken, nicht beim Bauen. */
1272
+ b0.onclick = function () { verlegeAlias(alt, neuesZiel()); };
1273
+ }
1274
+
1275
+ export function verlegeAlias(alt, neu) {
1276
+ if (!alt || !neu || alt === neu) { return; }
1277
+ var umfang = verlegeUmfang(alt);
1278
+ var b = $('#btn-move-go');
1279
+ b.disabled = true;
1280
+ b.textContent = tr('write.writing');
1281
+
1282
+ /* Erst alles Neue anlegen. Geht dabei etwas schief, steht der alte
1283
+ Alias noch unversehrt da - besser zwei als keiner. */
1284
+ /* Fehlende Ebenen ueber dem Ziel zuerst - das Erzeugen legt sie an
1285
+ (baueObjekte), das Verlegen liess sie aus: alias.0.Testecke gab es
1286
+ nach einem Umzug dorthin nur als Baumknoten, nicht als Objekt
1287
+ (Systemtest 25.08.2026). Gleiche Form wie dort: folder. */
1288
+ var arbeit = [];
1289
+ var teile = neu.split('.');
1290
+ for (var i = 3; i < teile.length; i++) {
1291
+ var pfad = teile.slice(0, i).join('.');
1292
+ if (!S.objects[pfad]) {
1293
+ arbeit.push({ id: pfad, obj: {
1294
+ type: 'folder',
1295
+ common: { name: teile[i - 1] },
1296
+ native: {}
1297
+ } });
1298
+ }
1299
+ }
1300
+ umfang.forEach(function (id) {
1301
+ var o = JSON.parse(JSON.stringify(S.objects[id]));
1302
+ o._id = neu + id.slice(alt.length);
1303
+ delete o.ts; delete o.from; delete o.user; delete o.acl;
1304
+ arbeit.push({ id: o._id, obj: o });
1305
+ });
1306
+ var offen = arbeit.length, fehler = [];
1307
+ arbeit.forEach(function (a) {
1308
+ var zielId = a.id;
1309
+ var o = a.obj;
1310
+ socket.emit('setObject', zielId, o, function (err) {
1311
+ if (err) { fehler.push(zielId + ': ' + err); }
1312
+ else {
1313
+ /* Sofort in den eigenen Bestand, nicht erst wenn das Abo es
1314
+ nachtraegt. Das Abo haelt sich zurueck, solange ein Dialog
1315
+ offen steht - und der steht hier offen. Ohne diese Zeile kannte
1316
+ die Werkbank ihren eben angelegten Alias nicht, `delKnopf`
1317
+ blieb aus, und ein zweites Verlegen war nicht moeglich. */
1318
+ S.objects[zielId] = o;
1319
+ }
1320
+ if (--offen === 0) { S.keysSorted = Object.keys(S.objects).sort(); weiter(); }
1321
+ });
1322
+ });
1323
+
1324
+ function weiter() {
1325
+ if (fehler.length) { return melden(fehler); }
1326
+
1327
+ /* Die Aufzaehlungen mitnehmen: alte Kennung raus, neue rein. Nur die
1328
+ eigene - alles andere darin gehoert anderen. */
1329
+ var enumArbeit = [];
1330
+ ['rooms', 'functions'].forEach(function (art) {
1331
+ enumsVon(alt, art).forEach(function (eid) {
1332
+ var o = JSON.parse(JSON.stringify(enums[eid]));
1333
+ var m = (o.common.members || []).filter(function (x) { return x !== alt; });
1334
+ if (m.indexOf(neu) === -1) { m.push(neu); m.sort(); }
1335
+ o.common.members = m;
1336
+ delete o.ts; delete o.from; delete o.user; delete o.acl;
1337
+ enumArbeit.push({ id: eid, obj: o });
1338
+ });
1339
+ });
1340
+
1341
+ var offen2 = enumArbeit.length;
1342
+ if (!offen2) { return loeschen(); }
1343
+ enumArbeit.forEach(function (a) {
1344
+ socket.emit('setObject', a.id, a.obj, function (err) {
1345
+ if (err) { fehler.push(a.id + ': ' + err); }
1346
+ if (--offen2 === 0) { loeschen(); }
1347
+ });
1348
+ });
1349
+ }
1350
+
1351
+ function loeschen() {
1352
+ /* Kinder vor den Eltern, sonst bleiben Waisen stehen. */
1353
+ var weg = umfang.slice().sort(function (a, b) { return b.length - a.length; });
1354
+ var offen3 = weg.length;
1355
+ weg.forEach(function (id) {
1356
+ socket.emit('delObject', id, function (err) {
1357
+ if (err) { fehler.push(id + ': ' + err); }
1358
+ if (--offen3 === 0) {
1359
+ weg.forEach(function (x) { delete S.objects[x]; });
1360
+ S.keysSorted = Object.keys(S.objects).sort();
1361
+ leereOrdnerRaeumen(alt, function () {
1362
+ S.current = neu;
1363
+ S.entwurf = null;
1364
+ S.verlegeZiel = null;
1365
+ melden(fehler, neu);
1366
+ });
1367
+ }
1368
+ });
1369
+ });
1370
+ }
1371
+
1372
+ function melden(f, zielId) {
1373
+ var body = $('#move-body');
1374
+ body.textContent = '';
1375
+ var m = el('div');
1376
+ m.style.fontSize = '13px';
1377
+ if (f.length) {
1378
+ m.appendChild(el('b', null, tr('write.failed', f.length)));
1379
+ var ul = el('ul');
1380
+ f.forEach(function (x) { ul.appendChild(el('li', null, x)); });
1381
+ m.appendChild(ul);
1382
+ } else {
1383
+ m.appendChild(el('b', null, tr('move.done', zielId)));
1384
+ }
1385
+ body.appendChild(m);
1386
+ /* Nur in diesem Dialog verschwinden lassen - beim naechsten Oeffnen
1387
+ baut ihn `zeigeVerlegen` wieder auf. Vorher blieb er dauerhaft
1388
+ versteckt, und ein zweites Verlegen ging nicht mehr. */
1389
+ b.hidden = true;
1390
+ b.disabled = false;
1391
+ ladeEnums(function () {
1392
+ zeichneBaum();
1393
+ /* Den Alias unter der neuen Kennung wieder aufschlagen.
1394
+
1395
+ `current` allein reicht nicht: der Entwurf haengt an der alten
1396
+ Kennung und wurde geloescht, also zeichnete `zeichneErgebnis`
1397
+ die leere rechte Seite - obwohl der Baum den neuen Knoten schon
1398
+ markiert hatte. Wer danach nochmal verlegen wollte, fand keinen
1399
+ Knopf und musste den Alias erst wieder anklicken. `waehle` baut
1400
+ den Entwurf neu auf, und damit steht auch der Knopf wieder da. */
1401
+ if (zielId && S.objects[zielId]) { waehle(zielId); }
1402
+ else { zeichneErgebnis(); }
1403
+ });
1404
+ }
1405
+ }
1406
+
1407
+ export function delKnopf(zeigen) {
1408
+ var b = $('#btn-del');
1409
+ if (b) { b.hidden = !zeigen; }
1410
+ /* Verlegen und Quellentausch gelten fuer dasselbe: einen Alias, den
1411
+ es gibt. */
1412
+ var m = $('#btn-move');
1413
+ if (m) { m.hidden = !zeigen; }
1414
+ var t = $('#btn-swap');
1415
+ if (t) { t.hidden = !zeigen; }
1416
+ }
1417
+
1418
+ export function leereOrdnerRaeumen(weg, fertig) {
1419
+ var teile = weg.split('.');
1420
+ var kette = [];
1421
+ for (var i = teile.length - 1; i > 2; i--) {
1422
+ kette.push(teile.slice(0, i).join('.'));
1423
+ }
1424
+ var i2 = 0;
1425
+ (function weiter() {
1426
+ if (i2 >= kette.length) { return fertig(); }
1427
+ var pfad = kette[i2++];
1428
+ var o = S.objects[pfad];
1429
+ var belegt = S.keysSorted.some(function (id) { return id.indexOf(pfad + '.') === 0; });
1430
+ if (!o || o.type !== 'folder' || belegt) { return fertig(); }
1431
+ socket.emit('delObject', pfad, function () {
1432
+ delete S.objects[pfad];
1433
+ S.keysSorted = Object.keys(S.objects).sort();
1434
+ weiter();
1435
+ });
1436
+ })();
1437
+ }
1438
+
1439
+ /* Wirklich schreiben — nur aus dem Trockenlauf heraus erreichbar. */
1440
+
1441
+ export function schreibeObjekte() {
1442
+ if (!S.entwurf) { return; }
1443
+ var entwuerfe2 = zuSchreiben(S.trockenAlle);
1444
+ var liste = [];
1445
+ var schlecht = 0;
1446
+ entwuerfe2.forEach(function (en) {
1447
+ baueObjekte(en).forEach(function (o) {
1448
+ if (!liste.some(function (x) { return x.id === o.id; })) { liste.push(o); }
1449
+ });
1450
+ schlecht += pruefeSchreiben(en).length;
1451
+ });
1452
+ if (schlecht) { return; }
1453
+
1454
+ var body = $('#dry-body');
1455
+ var offen = liste.length;
1456
+ var fehler = [];
1457
+ var fertig = 0;
1458
+
1459
+ $('#btn-dry-write').disabled = true;
1460
+ $('#btn-dry-write').textContent = tr('write.writing');
1461
+ /* Vor dem Schreiben festhalten: danach gibt es das Ziel, und die Frage
1462
+ „war das ein Update?" beantwortete sich sonst immer mit ja. */
1463
+ var warUpdate = !!S.objects[S.entwurf.ziel || S.entwurf.kanal];
1464
+
1465
+ var loeschen = S.loeschListe.filter(function (v) { return v.vorgabe; });
1466
+ offen += loeschen.length;
1467
+ loeschen.forEach(function (v) {
1468
+ socket.emit('delObject', v.id, function (err) {
1469
+ if (err) { fehler.push(v.id + ': ' + err); } else { fertig++; }
1470
+ if (--offen === 0) { abschluss(); }
1471
+ });
1472
+ });
1473
+
1474
+ liste.forEach(function (x) {
1475
+ socket.emit('setObject', x.id, x.obj, function (err) {
1476
+ if (err) { fehler.push(x.id + ': ' + err); } else { fertig++; }
1477
+ if (--offen === 0) { abschluss(); }
1478
+ });
1479
+ });
1480
+
1481
+ function abschluss() {
1482
+ body.textContent = '';
1483
+ var m = el('div');
1484
+ m.style.fontSize = '13px';
1485
+ if (fehler.length) {
1486
+ m.appendChild(el('b', null, tr('write.failed', fehler.length)));
1487
+ var ul = el('ul');
1488
+ fehler.forEach(function (t) { ul.appendChild(el('li', null, t)); });
1489
+ m.appendChild(ul);
1490
+ } else {
1491
+ m.appendChild(el('b', null, loeschen.length
1492
+ ? tr('write.doneWithRemoved', fertig, loeschen.length)
1493
+ : tr('write.done', fertig)));
1494
+ m.appendChild(el('div', null, tr('write.aliasNowAt', S.entwurf.ziel || S.entwurf.kanal)));
1495
+ }
1496
+ body.appendChild(m);
1497
+
1498
+ /* Der Dialog bleibt nach dem Schreiben stehen, damit man das Ergebnis
1499
+ liest. Was dann noch dastand, passte aber nicht mehr: die
1500
+ Ueberschrift sprach weiter davon, was entstehen *wuerde*, und der
1501
+ Knopf trug wieder „Jetzt anlegen" - deaktiviert, weil das Schreiben
1502
+ ihn gesperrt hatte und niemand ihn wieder freigab. Ein Knopf, der
1503
+ nichts mehr tun kann, gehoert weg, nicht ausgegraut hin.
1504
+
1505
+ Ging etwas schief, bleibt er stehen und heisst „Noch einmal
1506
+ versuchen" - da hat er wieder eine Aufgabe. */
1507
+ var zt2 = $('#dry-titel');
1508
+ if (fehler.length) {
1509
+ if (zt2) { zt2.textContent = tr('write.dryTitleFailed'); }
1510
+ $('#btn-dry-write').hidden = false;
1511
+ $('#btn-dry-write').disabled = false;
1512
+ $('#btn-dry-write').textContent = tr('write.retry');
1513
+ } else {
1514
+ if (zt2) {
1515
+ zt2.textContent = warUpdate ? tr('write.dryTitleDoneUpdate')
1516
+ : tr('write.dryTitleDoneCreate');
1517
+ }
1518
+ $('#btn-dry-write').hidden = true;
1519
+ }
1520
+ S.loeschListe = [];
1521
+
1522
+ /* Die Marke „geaendert“ hat ihre Aufgabe erfuellt.
1523
+
1524
+ Sie sagt „das hast du angefasst, es steht noch nicht so am Alias“.
1525
+ Nach dem Schreiben steht es so am Alias — die Marke blieb aber
1526
+ stehen, bis man den Reiter neu lud, und behauptete damit eine
1527
+ Abweichung, die es nicht mehr gibt. Nur bei Erfolg loeschen: ging
1528
+ etwas schief, ist die Aenderung tatsaechlich noch nicht drin. */
1529
+ if (!fehler.length && S.entwurf && S.entwurf.states) {
1530
+ S.entwurf.states.forEach(function (s) { s.geaendert = false; rateBehalten(s); });
1531
+ S.entwurf.rateMeldung = null;
1532
+ }
1533
+
1534
+ /* Nur der Aliaszweig, nicht die ganze Datenbank. */
1535
+ holeZweig((S.entwurf.ziel || S.entwurf.kanal).split('.').slice(0, 2).join('.') + '.');
1536
+ }
1537
+ }