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,1907 @@
1
+ /* Die dritte Sicht: Vorlagen ansehen, bearbeiten, speichern, Probelauf.
2
+
3
+ Paketvorlagen sind nur zu lesen - wer eine aendern will, dupliziert
4
+ sie, und die Kopie verdeckt das Original. */
5
+
6
+ import { S } from './zustand.js';
7
+ import { socket } from './verbindung.js';
8
+ import { D, $, el, kurz } from './basis.js';
9
+ import { tr, sprachtext, sprache } from './sprache.js';
10
+ import { katalogFehlt } from './katalog.js';
11
+ import { funktionsAuswahl, kennungZuFunktion } from './aufzaehlungen.js';
12
+ import { musterVon } from './erkennung.js';
13
+ import { musterZeile, musterName } from './musternamen.js';
14
+ import { ladeVorlagen, aendereVorlagen, setzeMeta, pruefeVorlage, INSTANZ_ID } from './vorlagen.js';
15
+ import { kindZustaende, hatPunkt, feldAusFormel } from './werte.js';
16
+ import { opt } from './entwurf.js';
17
+ import { zeichneErgebnis } from './ergebnis.js';
18
+ import { delKnopf } from './schreiben.js';
19
+
20
+ import { rollenFeld } from './rollenwahl.js';
21
+
22
+
23
+ /* ================== Vorlagen verwalten ==================
24
+ Dritte Sicht neben Quellen und Aliasen: links die Vorlagen, rechts
25
+ die gewaehlte mit Probelauf. Paketvorlagen sind nur zu lesen —
26
+ wer eine aendern will, dupliziert sie, und die Kopie verdeckt das
27
+ Original. Damit bleibt das Paket immer der Auslieferungszustand. */
28
+
29
+ var vGewaehlt = null; /* id der gewaehlten Vorlage */
30
+ var vEinlesen = null; /* eingelesene, noch nicht gespeicherte Vorlage */
31
+ var vEntwurf = null; /* Arbeitskopie beim Bearbeiten */
32
+ var vLoeschFrage = false;
33
+ var vJsonAuf = false;
34
+ var vOffeneZeile = null;
35
+
36
+ export function vorlageMitId(id) {
37
+ var t = null;
38
+ S.VORLAGEN.forEach(function (v) { if (v.id === id) { t = v; } });
39
+ return t;
40
+ }
41
+
42
+ function ohneUnterstrich(v) {
43
+ var k = {};
44
+ Object.keys(v).forEach(function (f) { if (f.charAt(0) !== '_') { k[f] = v[f]; } });
45
+ /* Halbfertige Ersatzquellen nicht mitspeichern: eine Zeile ohne
46
+ Datenpunkt ist beim Bearbeiten nuetzlich, in der Vorlage waere sie
47
+ nur Ballast. */
48
+ if (Array.isArray(k.zustaende)) {
49
+ k.zustaende = k.zustaende.map(function (z) {
50
+ if (!z || !z.lesenSonst) { return z; }
51
+ var liste = ersatzListe(z).filter(function (x) { return x && x.punkt; });
52
+ var kopie = {};
53
+ Object.keys(z).forEach(function (f) { kopie[f] = z[f]; });
54
+ if (!liste.length) { delete kopie.lesenSonst; }
55
+ else { kopie.lesenSonst = (liste.length === 1) ? liste[0] : liste; }
56
+ return kopie;
57
+ });
58
+ }
59
+ return k;
60
+ }
61
+
62
+ export function zeichneVorlagenListe() {
63
+ var host = $('#tree');
64
+ host.textContent = '';
65
+ var filter = ($('#q').value || '').trim().toLowerCase();
66
+
67
+ var liste = S.VORLAGEN.filter(function (v) {
68
+ if (!filter) { return true; }
69
+ return (v.id + ' ' + sprachtext(v.name)).toLowerCase().indexOf(filter) > -1;
70
+ });
71
+
72
+ if (vEinlesen) {
73
+ host.appendChild(vListenZeile(vEinlesen, true));
74
+ }
75
+ if (!liste.length && !vEinlesen) {
76
+ host.appendChild(el('div', 'empty', tr('tree.nothingFound')));
77
+ return;
78
+ }
79
+ liste.forEach(function (v) { host.appendChild(vListenZeile(v, false)); });
80
+ }
81
+
82
+ function vListenZeile(v, eingelesen) {
83
+ /* Zwei verschiedene Dinge, die im Objektbaum zwei verschiedene Klassen
84
+ tragen: `pick` heisst waehlbar (Mauszeiger), `sel` heisst gewaehlt
85
+ (Akzentfarbe). Diese Liste setzte nur `pick` — und `pick` faerbt
86
+ nichts. Die angeklickte Vorlage stand deshalb unmarkiert da, waehrend
87
+ rechts ihr Blatt aufgeschlagen war (26.08.2026). */
88
+ var gewaehlt = (!eingelesen && v.id === vGewaehlt && !vEinlesen) ||
89
+ (eingelesen && vEinlesen);
90
+ var d = el('div', 'node pick' + (gewaehlt ? ' sel' : ''));
91
+ d.style.cursor = 'pointer';
92
+ d.style.gap = '6px';
93
+ var eigen = v._quelle === 'benutzer';
94
+ d.appendChild(el('span', 'chip ' + (eingelesen ? 'warn' : (eigen ? 'ok' : 'mut')),
95
+ eingelesen ? tr('tv.imported') : (eigen ? tr('tpl.own') : tr('tv.package'))));
96
+ var nm = el('span', 'nm', sprachtext(v.name));
97
+ d.appendChild(nm);
98
+ var meta = el('span', null, v.id + ' v' + (v.version || 1));
99
+ meta.style.fontFamily = 'var(--mono)';
100
+ meta.style.fontSize = '10px';
101
+ meta.style.color = 'var(--ink-3)';
102
+ meta.style.marginLeft = 'auto';
103
+ d.appendChild(meta);
104
+ d.addEventListener('click', function () {
105
+ if (!eingelesen) { vEinlesen = null; vGewaehlt = v.id; }
106
+ vEntwurf = null;
107
+ vLoeschFrage = false;
108
+ vOffeneZeile = null;
109
+ zeichneVorlagenListe();
110
+ zeichneErgebnis();
111
+ });
112
+ return d;
113
+ }
114
+
115
+ /* Beim Bearbeiten wird auf einer Kopie gearbeitet — erst Speichern
116
+ macht sie echt. Gleiche Regel wie beim Entwurf eines Geraets. */
117
+ function vArbeitsstand() {
118
+ if (vEinlesen) { return vEinlesen; }
119
+ if (vEntwurf) { return vEntwurf; }
120
+ return vorlageMitId(vGewaehlt);
121
+ }
122
+
123
+ function vAendern(feld, wert) {
124
+ if (!vEinlesen && !vEntwurf) {
125
+ var o = vorlageMitId(vGewaehlt);
126
+ if (!o) { return; }
127
+ vEntwurf = JSON.parse(JSON.stringify(ohneUnterstrich(o)));
128
+ vEntwurf._quelle = o._quelle;
129
+ }
130
+ feld(vEinlesen || vEntwurf, wert);
131
+ zeichneErgebnis();
132
+ }
133
+
134
+ export function zeichneVorlagenBlatt(host) {
135
+ /* Die Knoepfe unten gehoeren zum Geraet, nicht zur Vorlage. In dieser
136
+ Sicht waeren sie sonst noch auf das Geraet scharf, das vor dem
137
+ Umschalten gewaehlt war. */
138
+ /* Nicht nur sperren, sondern verstecken — ein grauer Knopf „Alias
139
+ erzeugen" gehoert nicht unter eine Vorlage. */
140
+ ['#btn-dry', '#btn-tpl', '#btn-checks'].forEach(function (w) {
141
+ var b = $(w);
142
+ if (b) { b.disabled = true; b.hidden = true; }
143
+ });
144
+ var ba0 = $('#btn-dry-alle');
145
+ if (ba0) { ba0.hidden = true; }
146
+ delKnopf(false);
147
+
148
+ var v = vArbeitsstand();
149
+ if (!v) {
150
+ /* Dieselbe Erklaerung wie in den anderen beiden Modi. */
151
+ var lh = el('div', 'empty leerhilfe');
152
+ lh.appendChild(el('div', 'lh1', tr('help.templates')));
153
+ host.appendChild(lh);
154
+ return;
155
+ }
156
+ var eigen = v._quelle === 'benutzer' || !!vEinlesen;
157
+ var vorhanden = vorlageMitId(v.id);
158
+ var geaendert = !!vEntwurf || !!vEinlesen;
159
+
160
+ /* --- Kopf --- */
161
+ var head = el('div', 'reshead');
162
+ var links = el('div');
163
+ links.appendChild(el('div', 'ziel', sprachtext(v.name)));
164
+ var unter = el('div', 'unter');
165
+ unter.appendChild(el('span', 'chip ' + (eigen ? 'ok' : 'mut'),
166
+ vEinlesen ? tr('tv.imported') : (eigen ? tr('tpl.own') : tr('tv.package'))));
167
+ unter.appendChild(el('span', null, v.id + ' v' + (v.version || 1) +
168
+ ' · ' + tr('tv.rank', v.rang || 0)));
169
+ links.appendChild(unter);
170
+ head.appendChild(links);
171
+ host.appendChild(head);
172
+
173
+ /* --- Was das Einlesen bewirken wuerde --- */
174
+ if (vEinlesen) {
175
+ var iw = el('div', 'aside w');
176
+ iw.style.marginBottom = '11px';
177
+ iw.appendChild(el('b', null, tr('tv.importTitle')));
178
+ var was;
179
+ if (!vorhanden) { was = tr('tv.importNew', v.id); }
180
+ else if (vorhanden._quelle === 'benutzer') {
181
+ was = tr('tv.importReplaces', v.id, vorhanden.version || 1, v.version || 1);
182
+ } else { was = tr('tv.importShadows', v.id); }
183
+ iw.appendChild(document.createTextNode(' ' + was));
184
+ host.appendChild(iw);
185
+ } else if (!eigen) {
186
+ var pw = el('div', 'aside');
187
+ pw.style.marginBottom = '11px';
188
+ pw.appendChild(el('b', null, tr('tv.packageTitle')));
189
+ pw.appendChild(document.createTextNode(' ' + tr('tv.packageText')));
190
+ host.appendChild(pw);
191
+ }
192
+
193
+ /* --- Knoepfe. Ganz oben, weil man dort zuerst hinschaut. --- */
194
+ var bar = el('div');
195
+ bar.style.display = 'flex';
196
+ bar.style.gap = '8px';
197
+ bar.style.flexWrap = 'wrap';
198
+ bar.style.alignItems = 'center';
199
+ bar.style.margin = '0 0 12px';
200
+
201
+ if (geaendert) {
202
+ var bs = el('button', 'btn primary', vEinlesen ? tr('tv.importAndSave') : tr('tv.save'));
203
+ bs.addEventListener('click', vSpeichern);
204
+ bar.appendChild(bs);
205
+ var bv = el('button', 'btn', tr('tv.discard'));
206
+ bv.addEventListener('click', function () {
207
+ vEinlesen = null; vEntwurf = null; vLoeschFrage = false;
208
+ zeichneVorlagenListe(); zeichneErgebnis();
209
+ });
210
+ bar.appendChild(bv);
211
+ }
212
+
213
+ /* Zwei verschiedene Absichten, deshalb zwei Beschriftungen: eine
214
+ mitgelieferte Vorlage will man meist *aendern* — dann behaelt die
215
+ Kopie die Kennung und verdeckt das Original. Eine eigene Vorlage
216
+ will man meist *abwandeln* — dann braucht die Kopie eine neue
217
+ Kennung, sonst ueberschriebe sie das Vorbild. */
218
+ var bd2 = el('button', 'btn', eigen ? tr('tv.duplicate') : tr('tv.makeOwn'));
219
+ bd2.addEventListener('click', function () { vDuplizieren(v, !eigen); });
220
+ bar.appendChild(bd2);
221
+
222
+ var ba = el('button', 'btn', tr('tv.export'));
223
+ ba.addEventListener('click', function () { vAusgeben(v); });
224
+ bar.appendChild(ba);
225
+
226
+ if (eigen && !vEinlesen && vorhanden && vorhanden._quelle === 'benutzer') {
227
+ var bl = el('button', 'btn', vLoeschFrage ? tr('tv.deleteSure') : tr('tv.delete'));
228
+ if (vLoeschFrage) {
229
+ bl.style.background = 'var(--bad)';
230
+ bl.style.borderColor = 'var(--bad)';
231
+ bl.style.color = '#fff';
232
+ }
233
+ bl.addEventListener('click', function () {
234
+ if (!vLoeschFrage) { vLoeschFrage = true; zeichneErgebnis(); return; }
235
+ vLoeschen(v.id);
236
+ });
237
+ bar.appendChild(bl);
238
+ /* Verdeckt sie eine mitgelieferte, kommt die wieder zum Vorschein. */
239
+ if (vLoeschFrage) { bar.appendChild(el('span', 'hint', tr('tv.deleteHint'))); }
240
+ }
241
+ host.appendChild(bar);
242
+
243
+ /* --- Kopfdaten --- */
244
+ var kk = el('div', 'card');
245
+ kk.style.marginBottom = '11px';
246
+ var kh = el('div', 'ch');
247
+ kh.appendChild(el('span', 'typ', tr('tpls.head')));
248
+ if (geaendert) { kh.appendChild(el('span', 'chip warn', tr('tv.unsaved'))); }
249
+
250
+ kk.appendChild(kh);
251
+ var kb = el('div');
252
+ kb.style.padding = '9px 12px 11px';
253
+ kb.style.display = 'flex';
254
+ kb.style.flexWrap = 'wrap';
255
+ kb.style.gap = '9px 14px';
256
+
257
+ function feld(label, wert, setzen, breit) {
258
+ var l = el('label', 'feld');
259
+ l.appendChild(el('span', 'feldlabel', label));
260
+ var i = document.createElement('input');
261
+ i.className = 'tx';
262
+ i.type = 'text';
263
+ i.value = wert === undefined || wert === null ? '' : String(wert);
264
+ i.disabled = !eigen;
265
+ if (breit) { i.style.minWidth = breit; }
266
+ i.addEventListener('change', function () { vAendern(setzen, i.value); });
267
+ l.appendChild(i);
268
+ return l;
269
+ }
270
+
271
+ kb.appendChild(feld(tr('tpls.nameDe'), textIn(v.name, 'de'),
272
+ function (x, w) { if (typeof x.name === 'string') { x.name = { de: x.name, en: x.name }; } x.name.de = w; }, '140px'));
273
+ kb.appendChild(feld(tr('tpls.nameEn'), textIn(v.name, 'en'),
274
+ function (x, w) { if (typeof x.name === 'string') { x.name = { de: x.name, en: x.name }; } x.name.en = w; }, '140px'));
275
+ kb.appendChild(feld(tr('tpls.rank'), v.rang || 0,
276
+ function (x, w) { x.rang = Number(w) || 0; }, '60px'));
277
+ var rh = el('span', 'hint', eigen ? tr('tv.rankOwn') : tr('tv.rankPackage'));
278
+ rh.style.flexBasis = '100%';
279
+ kb.appendChild(rh);
280
+
281
+ var lt = el('label', 'feld');
282
+ lt.appendChild(el('span', 'feldlabel', tr('tpls.deviceType')));
283
+ var st = el('select', 'tx');
284
+ st.disabled = !eigen;
285
+ st.appendChild(opt('', tr('tpls.noType')));
286
+ (D ? Object.keys(D.patterns).sort() : []).forEach(function (m) {
287
+ var o = opt(m, musterZeile(m));
288
+ if (m === v.geraetetyp) { o.selected = true; }
289
+ st.appendChild(o);
290
+ });
291
+ st.addEventListener('change', function () {
292
+ vAendern(function (x, w) { x.geraetetyp = w || undefined; }, st.value);
293
+ });
294
+ lt.appendChild(st);
295
+ kb.appendChild(lt);
296
+
297
+ /* Wozu das Geraet zaehlt.
298
+
299
+ Nicht dasselbe wie der Geraetetyp: `socket` sagt nur, dass etwas
300
+ geschaltet wird - ob eine Steckdose, eine Lampe oder eine
301
+ Druckerleiste, weiss allein die Vorlage. Gespeichert wird die
302
+ Kennung aus dem Katalog des Admin, nicht der Anzeigename; daraus
303
+ loest die Werkbank beides auf, den vorhandenen Eintrag und die
304
+ Vorlage samt Uebersetzung und Bild. Leer heisst: nichts
305
+ vorschlagen. */
306
+ var lf = el('label', 'feld');
307
+ lf.appendChild(el('span', 'feldlabel', tr('tpls.function')));
308
+ var sf = el('select', 'tx');
309
+ sf.disabled = !eigen;
310
+ sf.appendChild(opt('', tr('tpls.noFunction')));
311
+ funktionsAuswahl(v.funktion).forEach(function (x) {
312
+ var o = opt(x.wert, x.text);
313
+ if (x.gewaehlt) { o.selected = true; }
314
+ sf.appendChild(o);
315
+ });
316
+ sf.addEventListener('change', function () {
317
+ vAendern(function (x, w) { x.funktion = w || undefined; }, sf.value);
318
+ });
319
+ lf.appendChild(sf);
320
+ if (katalogFehlt) { lf.appendChild(el('span', 'hint', tr('catalog.missing'))); }
321
+ kb.appendChild(lf);
322
+
323
+ kk.appendChild(kb);
324
+
325
+ var nb = el('div');
326
+ nb.style.padding = '0 12px 11px';
327
+ nb.appendChild(feld(tr('tpls.nameHint'), (v.erkennung || {}).namenshinweis || '',
328
+ function (x, w) { x.erkennung = x.erkennung || {}; if (w) { x.erkennung.namenshinweis = w; } else { delete x.erkennung.namenshinweis; } }, '260px'));
329
+ nb.appendChild(el('div', 'hint', tr('tv.nameHintShort')));
330
+ kk.appendChild(nb);
331
+ host.appendChild(kk);
332
+
333
+ /* --- Erkennung --- */
334
+ var ek = el('div', 'card');
335
+ ek.style.marginBottom = '11px';
336
+ var eh = el('div', 'ch');
337
+ eh.appendChild(el('span', 'typ', tr('tv.detection')));
338
+ var erf = (v.erkennung || {}).erforderlich || [];
339
+ eh.appendChild(el('span', 'chip mut', tr('tv.requiredCount', erf.length)));
340
+ ek.appendChild(eh);
341
+ var eb = el('div');
342
+ eb.style.padding = '9px 12px 11px';
343
+ var listenFeld = function (label, wert, setzen, hilfe) {
344
+ var w = el('div');
345
+ w.style.marginBottom = '9px';
346
+ w.appendChild(feld(label, wert, setzen, '380px'));
347
+ if (hilfe) { w.appendChild(el('div', 'hint', hilfe)); }
348
+ return w;
349
+ };
350
+ var alsListe = function (text) {
351
+ return String(text).split(',').map(function (t) { return t.trim(); })
352
+ .filter(function (t) { return !!t; });
353
+ };
354
+ eb.appendChild(listenFeld(tr('tv.required2'), erf.join(', '), function (x, wert) {
355
+ x.erkennung = x.erkennung || {};
356
+ x.erkennung.erforderlich = alsListe(wert);
357
+ }, tr('tv.requiredHelp')));
358
+ if (!erf.length) {
359
+ eb.appendChild(el('div', 'hint', tr('tpls.noRequired')));
360
+ }
361
+ var inh = (v.erkennung || {}).inhalt || {};
362
+ Object.keys(inh).forEach(function (punkt) {
363
+ var z = el('div', 'hint', tr('tpls.contentRow', punkt, inh[punkt]));
364
+ z.style.fontFamily = 'var(--mono)';
365
+ eb.appendChild(z);
366
+ });
367
+ eb.appendChild(listenFeld(tr('tv.forbidden'), ((v.erkennung || {}).verboten || []).join(', '),
368
+ function (x, wert) {
369
+ x.erkennung = x.erkennung || {};
370
+ var l = alsListe(wert);
371
+ if (l.length) { x.erkennung.verboten = l; } else { delete x.erkennung.verboten; }
372
+ }, tr('tv.forbiddenHelp')));
373
+ if (v.mehrfach) {
374
+ eb.appendChild(el('div', 'hint', tr('tv.multi', v.kanalname || 'POWER%N%')));
375
+ }
376
+ ek.appendChild(eb);
377
+ host.appendChild(ek);
378
+
379
+ /* --- Datenpunkte --- */
380
+ var zk = el('div', 'card');
381
+ zk.style.marginBottom = '11px';
382
+ var zh = el('div', 'ch');
383
+ zh.appendChild(el('span', 'typ', tr('tpls.states')));
384
+ zh.appendChild(el('span', 'chip mut', tr('tpls.statesCount', (v.zustaende || []).length)));
385
+ if (eigen) {
386
+ zh.appendChild(el('span', 'luecke'));
387
+ zh.appendChild(el('span', 'hint', tr('tv.clickRow')));
388
+ }
389
+ zk.appendChild(zh);
390
+ (v.zustaende || []).forEach(function (z, nr) {
391
+ var auf = (vOffeneZeile === nr);
392
+ var r = el('div', 'slot' + (eigen ? ' vklick' : '') + (auf ? ' offen' : ''));
393
+ r.style.gridTemplateColumns = '1fr 1.1fr 1.2fr 90px 15px';
394
+ r.appendChild(el('span', 'sn', z.name));
395
+ r.appendChild(el('span', 'rx', z.rolle || '\u2014'));
396
+ var pw = el('span', 'rx');
397
+ pw.style.fontFamily = 'var(--mono)';
398
+ pw.appendChild(document.createTextNode(z.lesen + (z.feld ? ' \u00b7 ' + z.feld : '')));
399
+ /* Der Schreibpfad ist ein Pfad wie der Lesepfad davor, keine
400
+ Statusmarke - als Chip las er sich wie \u201eVorgabe" daneben. */
401
+ if (z.schreiben) { pw.appendChild(el('span', 'leise', ' \u2192 ' + z.schreiben)); }
402
+ if (z.absolut) { pw.appendChild(el('span', 'chip warn', tr('tpls.absolute'))); }
403
+ r.appendChild(pw);
404
+ /* Dieselben zwei Angaben wie im Speichern-Dialog, gleiche Richtung
405
+ und dieselben Woerter — vorher stand hier „Pflicht/freiwillig"
406
+ und dort „Vorgabe / muss da sein", fuer genau dieselbe Sache. */
407
+ var mm = el('span', 'rx');
408
+ if (!z.vorgabeAus) { mm.appendChild(el('span', 'chip mut', tr('tpls.colDefault'))); }
409
+ if (istPflichtpunkt(v, z)) { mm.appendChild(el('span', 'chip ok', tr('tpls.colRequired'))); }
410
+ r.appendChild(mm);
411
+ r.appendChild(el('span', 'ca2', eigen ? (auf ? '\u25be' : '\u25b8') : ''));
412
+ if (eigen) {
413
+ r.addEventListener('click', function () {
414
+ vOffeneZeile = auf ? null : nr;
415
+ zeichneErgebnis();
416
+ });
417
+ }
418
+ zk.appendChild(r);
419
+ if (eigen && auf) { zk.appendChild(vZeilenDetail(z, nr)); }
420
+ });
421
+
422
+ if (eigen) {
423
+ var np = el('div');
424
+ np.style.padding = '9px 12px 11px';
425
+ var nKnopf = el('button', 'btn schmal', tr('tv.addState'));
426
+ nKnopf.addEventListener('click', function () {
427
+ vAendern(function (x) {
428
+ x.zustaende = x.zustaende || [];
429
+ x.zustaende.push({ name: 'NEU', typ: 'mixed', lesen: '', optional: true });
430
+ vOffeneZeile = x.zustaende.length - 1;
431
+ });
432
+ });
433
+ np.appendChild(nKnopf);
434
+ zk.appendChild(np);
435
+ }
436
+ /* Das Rollenwissen ist kein Zeilenbestand - nur ein Vermerk, dass es
437
+ da ist und wo es greift. */
438
+ if (v.weiterePunkte && Object.keys(v.weiterePunkte).length) {
439
+ var ww = el('div', 'hint');
440
+ ww.style.padding = '7px 12px 10px';
441
+ ww.textContent = tr('tpls.wissen', Object.keys(v.weiterePunkte).length);
442
+ zk.appendChild(ww);
443
+ }
444
+ host.appendChild(zk);
445
+
446
+ /* --- Probelauf --- */
447
+ var pk = el('div', 'card');
448
+ pk.style.marginBottom = '11px';
449
+ var ph2 = el('div', 'ch');
450
+ ph2.appendChild(el('span', 'typ', tr('tpls.tryout')));
451
+ if (erf.length) { probeWerteHolen(ohneUnterstrich(v), zeichneErgebnis); }
452
+ var treffer = erf.length
453
+ ? probelauf(ohneUnterstrich(v), v._quelle === 'benutzer') : [];
454
+ var gew = treffer.filter(function (t) { return t.gewinnt; }).length;
455
+ ph2.appendChild(el('span', 'chip ' + (gew ? 'ok' : 'warn'), tr('tpls.hits', treffer.length, gew)));
456
+ pk.appendChild(ph2);
457
+ var pb2 = el('div');
458
+ pb2.style.padding = '9px 12px 11px';
459
+ if (!treffer.length) {
460
+ pb2.appendChild(el('div', 'hint', erf.length ? tr('tpls.noHits') : tr('tpls.noRequired')));
461
+ } else {
462
+ treffer.slice(0, 40).forEach(function (t) {
463
+ var r = el('div', 'slot');
464
+ r.style.gridTemplateColumns = '1fr 110px 150px';
465
+ var idl = el('span', 'sn', t.id);
466
+ idl.style.fontFamily = 'var(--mono)';
467
+ r.appendChild(idl);
468
+ r.appendChild(el('span', 'rx', tr('tpls.evidence', t.eigen.belege)));
469
+ r.appendChild(el('span', 'chip ' + (t.gewinnt ? 'ok' : 'bad'),
470
+ t.gewinnt ? (t.gegner ? tr('tpls.beats', textIn(t.gegner.vorlage.name, sprache)) : tr('tpls.onlyOne'))
471
+ : tr('tpls.losesTo', textIn(t.gegner.vorlage.name, sprache))));
472
+ pb2.appendChild(r);
473
+ });
474
+ if (treffer.length > 40) {
475
+ pb2.appendChild(el('div', 'hint', tr('tpls.andMore', treffer.length - 40)));
476
+ }
477
+ }
478
+ pk.appendChild(pb2);
479
+ host.appendChild(pk);
480
+
481
+ /* --- das JSON, zugeklappt: bei 16 Datenpunkten sind das 200 Zeilen,
482
+ und darunter faende niemand mehr die Knoepfe --- */
483
+ var jk = el('div', 'card');
484
+ jk.style.marginBottom = '11px';
485
+ var jh = el('div', 'ch');
486
+ jh.appendChild(el('span', 'typ', tr('tv.json')));
487
+ jh.appendChild(el('span', 'luecke'));
488
+ var jb = el('button', 'btn schmal', vJsonAuf ? tr('tv.hide') : tr('tv.show'));
489
+ jb.addEventListener('click', function () { vJsonAuf = !vJsonAuf; zeichneErgebnis(); });
490
+ jh.appendChild(jb);
491
+ jk.appendChild(jh);
492
+ if (vJsonAuf) {
493
+ var pre = el('div', 'raw');
494
+ pre.style.margin = '9px 12px 11px';
495
+ pre.style.whiteSpace = 'pre';
496
+ pre.style.maxHeight = 'none';
497
+ pre.textContent = JSON.stringify(ohneUnterstrich(v), null, 2);
498
+ jk.appendChild(pre);
499
+ }
500
+ host.appendChild(jk);
501
+
502
+ }
503
+
504
+ /* Ein Datenpunkt zum Aufklappen. Bewusst ohne Werte und ohne
505
+ Detektor-Bericht: hier gibt es kein Geraet, an dem man das messen
506
+ koennte. Was die Aenderung bewirkt, zeigt der Probelauf darunter. */
507
+ /* ============ Bausteine, die beide Detailansichten teilen ============
508
+ Der Block am Geraet und der an der Vorlage sollen gleich aussehen und
509
+ gleich bedienbar sein. Wo sie dasselbe zeigen, bauen sie es jetzt aus
510
+ denselben Funktionen — sonst laufen die beiden auseinander, sobald man
511
+ an einer Stelle etwas verbessert. */
512
+
513
+ /* Eine Zeile Beschriftung + Inhalt, wie im Detail am Geraet. */
514
+ export function dtZeile(d, k, inhalt, cls) {
515
+ var r = el('div', 'dr');
516
+ r.appendChild(el('div', 'k', k));
517
+ if (inhalt === undefined || inhalt === null) {
518
+ r.appendChild(el('div', cls || null, '\u2014'));
519
+ } else if (typeof inhalt === 'string' || typeof inhalt === 'number') {
520
+ r.appendChild(el('div', cls || null, String(inhalt)));
521
+ } else {
522
+ r.appendChild(inhalt);
523
+ }
524
+ d.appendChild(r);
525
+ return r;
526
+ }
527
+
528
+ function dtFeld(beschriftung, element) {
529
+ var l = el('label', 'fld');
530
+ l.appendChild(el('span', null, beschriftung));
531
+ l.appendChild(element);
532
+ return l;
533
+ }
534
+
535
+ /* Rollenauswahl samt Begruendung. geraetetyp darf leer sein — dann
536
+ entfaellt nur der Hinweis, welchen Platz die Rolle traefe. */
537
+ function rollenBlock(rolle, geraetetyp, setzen) {
538
+ var rb = el('div');
539
+ /* Dasselbe durchsuchbare Feld wie am Geraet - ein Aussehen fuer alle
540
+ Rollenwahlen. Ohne Belegt-Vermerk: in der Vorlage gibt es keinen
541
+ Entwurf, gegen den man Plaetze zaehlen koennte. */
542
+ rb.appendChild(rollenFeld({
543
+ wert: rolle || '',
544
+ muster: geraetetyp,
545
+ leerErlaubt: true,
546
+ breite: '300px',
547
+ beiWahl: function (r) { setzen(r); }
548
+ }));
549
+
550
+ var must = geraetetyp && musterVon(geraetetyp);
551
+ if (must) {
552
+ var treffer = null;
553
+ must.states.forEach(function (pp) {
554
+ if (!treffer && pp.role && rolle &&
555
+ new RegExp(pp.role.source ? pp.role.source : pp.role).test(rolle)) { treffer = pp; }
556
+ });
557
+ var hin = el('div', 'sugg');
558
+ if (treffer) {
559
+ hin.appendChild(document.createTextNode(tr('pattern.fitsOn')));
560
+ hin.appendChild(el('b', null, String(treffer.role)));
561
+ hin.appendChild(document.createTextNode(tr('pattern.toSlot', treffer.name, musterName(geraetetyp) || geraetetyp)));
562
+ } else {
563
+ hin.textContent = tr('pattern.noPlaceLong', musterName(geraetetyp) || geraetetyp);
564
+ }
565
+ rb.appendChild(hin);
566
+ }
567
+ return rb;
568
+ }
569
+
570
+ /* Typ, Einheit, Nachkommastellen, Beschriftung — in dieser Reihenfolge
571
+ und mit denselben Auswahlmoeglichkeiten wie am Geraet. */
572
+ function anzeigeBlock(hole, setze, platzhalter) {
573
+ var af = el('div', 'fields');
574
+
575
+ var selT = el('select', 'tx');
576
+ [['', '\u2014'], ['boolean', 'boolean'], ['number', 'number'],
577
+ ['string', 'string'], ['mixed', 'mixed']].forEach(function (o) {
578
+ selT.appendChild(opt(o[0], o[1]));
579
+ });
580
+ selT.value = hole('typ') || '';
581
+ selT.addEventListener('click', function (ev) { ev.stopPropagation(); });
582
+ selT.addEventListener('change', function () { setze('typ', selT.value); });
583
+ af.appendChild(dtFeld(tr('detail.type'), selT));
584
+
585
+ var iU = el('input', 'tx');
586
+ iU.type = 'text';
587
+ iU.style.width = '70px';
588
+ iU.value = hole('einheit') || '';
589
+ iU.placeholder = '\u2014';
590
+ iU.addEventListener('click', function (ev) { ev.stopPropagation(); });
591
+ iU.addEventListener('change', function () { setze('einheit', iU.value); });
592
+ af.appendChild(dtFeld(tr('detail.unit'), iU));
593
+
594
+ var selD = el('select', 'tx');
595
+ [['', tr('detail.asDelivered')], ['0', '0'], ['1', '1'], ['2', '2'], ['3', '3']]
596
+ .forEach(function (o) { selD.appendChild(opt(o[0], o[1])); });
597
+ var dec = hole('dec');
598
+ selD.value = (dec === undefined || dec === null) ? '' : String(dec);
599
+ selD.addEventListener('click', function (ev) { ev.stopPropagation(); });
600
+ selD.addEventListener('change', function () { setze('dec', selD.value); });
601
+ af.appendChild(dtFeld(tr('detail.decimals'), selD));
602
+
603
+ var iC = el('input', 'tx');
604
+ iC.type = 'text';
605
+ iC.style.width = '150px';
606
+ iC.value = hole('caption') || '';
607
+ iC.placeholder = platzhalter || '';
608
+ iC.addEventListener('click', function (ev) { ev.stopPropagation(); });
609
+ iC.addEventListener('change', function () { setze('caption', iC.value); });
610
+ af.appendChild(dtFeld(tr('detail.caption'), iC));
611
+
612
+ return af;
613
+ }
614
+
615
+ /* Zaehlt dieser Datenpunkt fuer die Erkennung? Nicht aus optional
616
+ abgelesen, sondern aus der Pflichtliste — die ist die Wahrheit. */
617
+ function istPflichtpunkt(v, z) {
618
+ var erf = (v.erkennung || {}).erforderlich || [];
619
+ return !!z.lesen && erf.indexOf(z.lesen) > -1;
620
+ }
621
+
622
+ /* lesenSonst darf ein Punktname, ein Objekt oder eine Liste sein — alle
623
+ drei Formen kommen in Vorlagen vor. Nach innen immer eine Liste. */
624
+ function ersatzListe(z) {
625
+ var s0 = z.lesenSonst;
626
+ if (!s0) { return []; }
627
+ var arr = Array.isArray(s0) ? s0 : [s0];
628
+ return arr.map(function (x) {
629
+ return (typeof x === 'string') ? { punkt: x } : x;
630
+ });
631
+ }
632
+
633
+ /* Im Arbeitsstand bleibt eine noch leere Zeile stehen — sonst koennte
634
+ man nie eine anlegen, weil sie im selben Atemzug wieder verschwaende.
635
+ Weggeworfen wird sie erst beim Ablegen, in ohneUnterstrich. */
636
+ function schreibeErsatz(z, liste) {
637
+ if (!liste.length) { delete z.lesenSonst; return; }
638
+ z.lesenSonst = (liste.length === 1) ? liste[0] : liste;
639
+ }
640
+
641
+ function vZeilenDetail(z, nr) {
642
+ var v = vArbeitsstand();
643
+ var d = el('div', 'detail');
644
+
645
+ /* Genau der Aufbau des Detailblocks am Geraet: Name, liest aus,
646
+ schreibt nach, Formel, Schreibformel, Rolle, Anzeige. Nur was ein
647
+ Geraet braucht, fehlt hier — Rohwert, Ergebnis und die Auswahl aus
648
+ vorhandenen Objekten. Es gibt kein Geraet, an dem man das messen
649
+ koennte. */
650
+ var setz = function (feldname, wandeln) {
651
+ return function (x, wert) {
652
+ var zz = x.zustaende[nr];
653
+ var w = wandeln ? wandeln(wert) : wert;
654
+ if (w === '' || w === undefined || w === null) { delete zz[feldname]; }
655
+ else { zz[feldname] = w; }
656
+ };
657
+ };
658
+ var setzeFeld = function (feldname, wert, wandeln) {
659
+ vAendern(setz(feldname, wandeln), wert);
660
+ };
661
+
662
+ var eingabe = function (wert, feldname, breite, platzhalter, wandeln) {
663
+ var i = el('input', 'tx');
664
+ i.type = 'text';
665
+ i.value = wert === undefined || wert === null ? '' : String(wert);
666
+ if (breite) { i.style.width = breite; }
667
+ if (platzhalter) { i.placeholder = platzhalter; }
668
+ i.addEventListener('click', function (ev) { ev.stopPropagation(); });
669
+ i.addEventListener('change', function () { setzeFeld(feldname, i.value, wandeln); });
670
+ return i;
671
+ };
672
+
673
+ dtZeile(d, tr('detail.name'), eingabe(z.name, 'name', '260px', tr('detail.namePlaceholder')));
674
+
675
+ /* --- liest aus, mit JSON-Feld daneben --- */
676
+ var qf = el('div', 'fields');
677
+ qf.appendChild(dtFeld(tr('detail.datapoint'),
678
+ eingabe(z.lesen, 'lesen', '260px', z.absolut ? '0_userdata.0.\u2026' : 'stat.POWER')));
679
+ qf.appendChild(dtFeld(tr('detail.jsonField'), eingabe(z.feld, 'feld', '150px', '\u2014')));
680
+ dtZeile(d, tr('detail.readsFrom'), qf);
681
+
682
+ /* --- Ersatzquellen ---
683
+ Dasselbe steht bei Tasmota oft an mehreren Stellen, und welche ein
684
+ Geraet gerade fuehrt, weiss man vorher nicht: die IP liegt in
685
+ tele.INFO2, in tele.STATE oder in stat.STATUS5. Die Werkbank nimmt
686
+ den ersten Weg, bei dem Objekt und Feld wirklich da sind. Ohne
687
+ dieses Feld liess sich das nur in der ausgegebenen Datei aendern. */
688
+ var sonstListe = ersatzListe(z);
689
+
690
+ var sb = el('div');
691
+ sonstListe.forEach(function (w, wi) {
692
+ var zeile = el('div', 'fields');
693
+ zeile.style.marginBottom = '5px';
694
+
695
+ var setzeErsatz = function (teil, wert) {
696
+ vAendern(function (x) {
697
+ var zz = x.zustaende[nr];
698
+ var liste = ersatzListe(zz);
699
+ if (!liste[wi]) { return; }
700
+ if (wert === '') { delete liste[wi][teil]; } else { liste[wi][teil] = wert; }
701
+ if (!liste[wi].punkt) { liste.splice(wi, 1); }
702
+ schreibeErsatz(zz, liste);
703
+ }, wert);
704
+ };
705
+
706
+ var ip1 = el('input', 'tx');
707
+ ip1.type = 'text'; ip1.value = w.punkt || ''; ip1.style.width = '210px';
708
+ ip1.placeholder = 'tele.STATE';
709
+ ip1.addEventListener('click', function (ev) { ev.stopPropagation(); });
710
+ ip1.addEventListener('change', function () { setzeErsatz('punkt', ip1.value); });
711
+ zeile.appendChild(dtFeld(tr('detail.datapoint'), ip1));
712
+
713
+ var ip2 = el('input', 'tx');
714
+ ip2.type = 'text'; ip2.value = w.feld || ''; ip2.style.width = '150px';
715
+ ip2.placeholder = '—';
716
+ ip2.addEventListener('click', function (ev) { ev.stopPropagation(); });
717
+ ip2.addEventListener('change', function () { setzeErsatz('feld', ip2.value); });
718
+ zeile.appendChild(dtFeld(tr('detail.jsonField'), ip2));
719
+
720
+ var ip3 = el('input', 'tx');
721
+ ip3.type = 'text'; ip3.value = w.leseformel || ''; ip3.style.width = '230px';
722
+ ip3.placeholder = tr('detail.formulaPlaceholder');
723
+ ip3.addEventListener('click', function (ev) { ev.stopPropagation(); });
724
+ ip3.addEventListener('change', function () { setzeErsatz('leseformel', ip3.value); });
725
+ zeile.appendChild(dtFeld(tr('detail.formula'), ip3));
726
+
727
+ var x1 = el('button', 'btn schmal', tr('tv.removeFallback'));
728
+ x1.type = 'button';
729
+ x1.addEventListener('click', function (ev) {
730
+ ev.stopPropagation();
731
+ vAendern(function (x) {
732
+ var zz = x.zustaende[nr];
733
+ var liste = ersatzListe(zz);
734
+ liste.splice(wi, 1);
735
+ schreibeErsatz(zz, liste);
736
+ }, null);
737
+ });
738
+ zeile.appendChild(dtFeld(' ', x1));
739
+
740
+ sb.appendChild(zeile);
741
+ });
742
+
743
+ var plus = el('button', 'btn schmal', tr('tv.addFallback'));
744
+ plus.type = 'button';
745
+ plus.addEventListener('click', function (ev) {
746
+ ev.stopPropagation();
747
+ vAendern(function (x) {
748
+ var zz = x.zustaende[nr];
749
+ var liste = ersatzListe(zz);
750
+ liste.push({ punkt: '' });
751
+ schreibeErsatz(zz, liste);
752
+ }, null);
753
+ });
754
+ sb.appendChild(plus);
755
+ sb.appendChild(el('div', 'sugg', tr('tv.fallbackHint')));
756
+ dtZeile(d, tr('tv.fallback'), sb);
757
+
758
+ dtZeile(d, tr('detail.writesTo'),
759
+ eingabe(z.schreiben, 'schreiben', '260px', tr('detail.noWrite')));
760
+
761
+ var fb = el('div');
762
+ fb.appendChild(eingabe(z.leseformel, 'leseformel', '100%', tr('detail.formulaPlaceholder')));
763
+ if ((z.typ || '') === 'boolean') {
764
+ fb.appendChild(el('div', 'sugg', tr('detail.formulaHint')));
765
+ }
766
+ dtZeile(d, tr('detail.formula'), fb);
767
+
768
+ if (z.schreiben) {
769
+ var wb = el('div');
770
+ wb.appendChild(eingabe(z.schreibformel, 'schreibformel', '100%', tr('detail.writeFormulaPlaceholder')));
771
+ wb.appendChild(el('div', 'sugg', tr('detail.writeFormulaHint')));
772
+ dtZeile(d, tr('detail.writeFormula'), wb);
773
+ }
774
+
775
+ dtZeile(d, tr('detail.role'), rollenBlock(z.rolle, v.geraetetyp, function (neu) {
776
+ setzeFeld('rolle', neu);
777
+ }));
778
+
779
+ dtZeile(d, tr('detail.display'), anzeigeBlock(
780
+ function (was) {
781
+ if (was === 'einheit') { return z.einheit; }
782
+ if (was === 'dec') { return z.nachkommastellen; }
783
+ if (was === 'caption') { return z.beschriftung; }
784
+ return z.typ;
785
+ },
786
+ function (was, wert) {
787
+ if (was === 'einheit') { return setzeFeld('einheit', wert); }
788
+ if (was === 'dec') { return setzeFeld('nachkommastellen', wert, function (w) { return w === '' ? '' : Number(w); }); }
789
+ if (was === 'caption') { return setzeFeld('beschriftung', wert); }
790
+ setzeFeld('typ', wert);
791
+ }, z.name));
792
+
793
+ var haken = function (label, an, tut) {
794
+ var l = el('label', 'fld');
795
+ l.style.flexDirection = 'row';
796
+ l.style.alignItems = 'center';
797
+ l.style.gap = '5px';
798
+ var c = document.createElement('input');
799
+ c.type = 'checkbox';
800
+ c.checked = an;
801
+ c.addEventListener('click', function (ev) { ev.stopPropagation(); });
802
+ c.addEventListener('change', function () { vAendern(function (x) { tut(x.zustaende[nr], c.checked); }); });
803
+ l.appendChild(c);
804
+ l.appendChild(el('span', null, label));
805
+ return l;
806
+ };
807
+
808
+ var zeile2 = el('div', 'fields');
809
+ /* Beide Haken laufen in dieselbe Richtung wie im Speichern-Dialog:
810
+ angehakt heisst „ja". Vorher stand hier „freiwillig" und „Vorgabe
811
+ aus" — also zweimal die Umkehrung, was niemand zusammenbringt. */
812
+ zeile2.appendChild(haken(tr('tpls.colDefault'), !z.vorgabeAus,
813
+ function (zz, an) { if (an) { delete zz.vorgabeAus; } else { zz.vorgabeAus = true; } }));
814
+
815
+ /* Ein Pflichtpunkt ist zweierlei in einem: er darf beim Anwenden
816
+ nicht fehlen (optional) und er entscheidet ueber die Erkennung
817
+ (erforderlich). Der Haken haelt beides zusammen — sonst koennten
818
+ Liste und Zeile Verschiedenes behaupten. */
819
+ zeile2.appendChild(haken(tr('tpls.colRequired'), istPflichtpunkt(v, z), function (zz, an) {
820
+ var w = vArbeitsstand();
821
+ w.erkennung = w.erkennung || {};
822
+ var liste = (w.erkennung.erforderlich || []).slice();
823
+ var pfad = zz.lesen;
824
+ var i = liste.indexOf(pfad);
825
+ if (an) {
826
+ delete zz.optional;
827
+ if (pfad && i === -1) { liste.push(pfad); }
828
+ if (zz.schreiben && liste.indexOf(zz.schreiben) === -1) { liste.push(zz.schreiben); }
829
+ } else {
830
+ zz.optional = true;
831
+ if (i > -1) { liste.splice(i, 1); }
832
+ var j = zz.schreiben ? liste.indexOf(zz.schreiben) : -1;
833
+ if (j > -1) { liste.splice(j, 1); }
834
+ }
835
+ w.erkennung.erforderlich = liste;
836
+ }));
837
+
838
+ zeile2.appendChild(haken(tr('tpls.absolute'), !!z.absolut,
839
+ function (zz, an) { if (an) { zz.absolut = true; } else { delete zz.absolut; } }));
840
+ var wKnopf = el('button', 'btn schmal', tr('tv.removeState'));
841
+ wKnopf.addEventListener('click', function () {
842
+ vAendern(function (x) { x.zustaende.splice(nr, 1); vOffeneZeile = null; });
843
+ });
844
+ zeile2.appendChild(wKnopf);
845
+ dtZeile(d, tr('tv.inTemplate'), zeile2);
846
+
847
+ var hw = el('div', 'sugg');
848
+ hw.textContent = tr('tv.flagsHelp');
849
+ dtZeile(d, '', hw);
850
+ return d;
851
+ }
852
+
853
+ function vDuplizieren(v, gleicheKennung) {
854
+ var k = JSON.parse(JSON.stringify(ohneUnterstrich(v)));
855
+ if (gleicheKennung) {
856
+ /* Uebernehmen statt abwandeln: gleiche Kennung, eine Version
857
+ weiter — damit verdeckt sie die mitgelieferte. */
858
+ k.version = (k.version || 1) + 1;
859
+ } else {
860
+ k.id = freierSchluessel(k.id + '-kopie');
861
+ k.version = 1;
862
+ k.rang = eigenerMaxRang() + 10;
863
+ k.name = { de: textIn(k.name, 'de') + ' (Kopie)', en: textIn(k.name, 'en') + ' (copy)' };
864
+ }
865
+ if (typeof k.name === 'string') { k.name = { de: k.name, en: k.name }; }
866
+ vEinlesen = null;
867
+ vEntwurf = k;
868
+ vEntwurf._quelle = 'benutzer';
869
+ vGewaehlt = k.id;
870
+ vLoeschFrage = false;
871
+ zeichneVorlagenListe();
872
+ zeichneErgebnis();
873
+ }
874
+
875
+ /* Ausgeben als Datei — dasselbe Format wie admin/vorlagen/*.json, damit
876
+ eine gut gewordene Vorlage ohne Umbau ins Paket wandern kann. */
877
+ function vAusgeben(v) {
878
+ var text = JSON.stringify(ohneUnterstrich(v), null, 2) + '\n';
879
+ var a = document.createElement('a');
880
+ a.href = URL.createObjectURL(new Blob([text], { type: 'application/json' }));
881
+ a.download = v.id + '.json';
882
+ document.body.appendChild(a);
883
+ a.click();
884
+ setTimeout(function () { URL.revokeObjectURL(a.href); a.remove(); }, 2000);
885
+ }
886
+
887
+ export function vEinlesenDatei(datei) {
888
+ var leser = new FileReader();
889
+ leser.onload = function () {
890
+ var roh;
891
+ try { roh = JSON.parse(String(leser.result)); } catch { roh = null; }
892
+ var fehler = vPruefeEingelesen(roh);
893
+ if (fehler) {
894
+ vEinlesen = null;
895
+ var host = $('#res');
896
+ host.textContent = '';
897
+ var w = el('div', 'aside w');
898
+ w.appendChild(el('b', null, tr('tv.importBad')));
899
+ w.appendChild(document.createTextNode(' ' + fehler));
900
+ host.appendChild(w);
901
+ return;
902
+ }
903
+ vEinlesen = ohneUnterstrich(roh);
904
+ vEinlesen._quelle = 'benutzer';
905
+ vEntwurf = null;
906
+ vLoeschFrage = false;
907
+ zeichneVorlagenListe();
908
+ zeichneErgebnis();
909
+ };
910
+ leser.readAsText(datei);
911
+ }
912
+
913
+ function vPruefeEingelesen(o) {
914
+ if (!o || typeof o !== 'object' || Array.isArray(o)) { return tr('tv.badNotObject'); }
915
+ if (!o.id || typeof o.id !== 'string') { return tr('tv.badNoId'); }
916
+ if (!Array.isArray(o.zustaende) || !o.zustaende.length) { return tr('tv.badNoStates'); }
917
+ var schlecht = o.zustaende.filter(function (z) { return !z || !z.name || !z.lesen; });
918
+ if (schlecht.length) { return tr('tv.badState', schlecht.length); }
919
+ return null;
920
+ }
921
+
922
+ function vSpeichern() {
923
+ var v = vArbeitsstand();
924
+ if (!v) { return; }
925
+ var rein = ohneUnterstrich(v);
926
+
927
+ /* Aendert sich der Inhalt, zaehlt die Version weiter. Sonst traegt
928
+ jedes Geraet fuer immer „vorlageVersion: 1" und gilt als aktuell —
929
+ ein spaeteres Nachziehen haette nie etwas zu melden. Das Duplizieren
930
+ zaehlte schon hoch, das Speichern nicht. */
931
+ var alt = vorlageMitId(rein.id);
932
+ if (alt) {
933
+ var ohneV = function (x) {
934
+ var k = JSON.parse(JSON.stringify(ohneUnterstrich(x)));
935
+ delete k.version;
936
+ return JSON.stringify(k);
937
+ };
938
+ /* Nie unter das fallen, was der Arbeitsstand schon traegt: beim
939
+ Duplizieren einer mitgelieferten Vorlage steht dort bereits eine
940
+ Version hoeher, damit die Kopie das Original verdeckt. Wuerde man
941
+ stumpf die gespeicherte uebernehmen, faende die Kopie zurueck auf
942
+ die Version des Pakets. */
943
+ var vorschlag = Number(rein.version) || 1;
944
+ var altV = Number(alt.version) || 1;
945
+ rein.version = (ohneV(alt) !== ohneV(rein))
946
+ ? Math.max(vorschlag, altV + 1)
947
+ : Math.max(vorschlag, altV);
948
+ }
949
+ aendereVorlagen(rein.id, rein, function (err) {
950
+ if (err) { return vMeldung(tr('tpls.failed') + ' ' + err, true); }
951
+ ladeVorlagen(function () {
952
+ vEinlesen = null; vEntwurf = null; vGewaehlt = rein.id;
953
+ setzeMeta();
954
+ zeichneVorlagenListe();
955
+ zeichneErgebnis();
956
+ });
957
+ });
958
+ }
959
+
960
+ function vLoeschen(id) {
961
+ aendereVorlagen(id, null, function (err) {
962
+ if (err) { return vMeldung(tr('tpls.failed') + ' ' + err, true); }
963
+ ladeVorlagen(function () {
964
+ vEinlesen = null; vEntwurf = null; vLoeschFrage = false;
965
+ vGewaehlt = vorlageMitId(id) ? id : null;
966
+ setzeMeta();
967
+ zeichneVorlagenListe();
968
+ zeichneErgebnis();
969
+ });
970
+ });
971
+ }
972
+
973
+ function vMeldung(text, schlecht) {
974
+ var host = $('#res');
975
+ host.textContent = '';
976
+ var w = el('div', 'aside' + (schlecht ? ' w' : ''));
977
+ w.appendChild(el('b', null, text));
978
+ host.appendChild(w);
979
+ }
980
+
981
+ /* ================== Aus einem Geraet eine Vorlage machen ==================
982
+ Die Gegenrichtung zu wendeAn. Ableitbar ist, welche Punkte benutzt
983
+ werden und wie sie gelesen werden. Nicht ableitbar ist, welche davon
984
+ ein Geraet zu diesem Geraetetyp machen und woran man es am Namen
985
+ erkennt — danach fragt der Dialog. */
986
+
987
+
988
+ export function textIn(n, l) {
989
+ if (!n) { return ''; }
990
+ if (typeof n === 'string') { return n; }
991
+ return n[l] || n.en || n.de || '';
992
+ }
993
+
994
+ function relZu(id, kanal) {
995
+ if (id && id.indexOf(kanal + '.') === 0) { return { p: kurz(id, kanal), abs: false }; }
996
+ return { p: id || '', abs: true };
997
+ }
998
+
999
+ /* stat.POWER1 → stat.POWER%N%. Ersetzt wird nur am Ende eines
1000
+ Abschnitts und nur, wenn davor noch etwas steht — sonst wuerde aus
1001
+ ENERGY.Power beim Speichern Unsinn. */
1002
+ function zuPlatzhalter(pfad, inst) {
1003
+ var i = (inst === null || inst === undefined) ? '' : String(inst);
1004
+ if (!i) { return pfad; }
1005
+ return String(pfad).split('.').map(function (t) {
1006
+ return (t.length > i.length && t.slice(-i.length) === i) ? t.slice(0, -i.length) + '%N%' : t;
1007
+ }).join('.');
1008
+ }
1009
+
1010
+ /* Welcher Pfad steht am Ende in der Vorlage? Bei einem Mehrfachgeraet
1011
+ tritt der Platzhalter an die Stelle der Ausgangsnummer. */
1012
+ function zPfad(r, k, schreibend) {
1013
+ var roh = schreibend ? r.schreibenRoh : r.lesenRoh;
1014
+ var abs = schreibend ? r.schreibenAbs : r.lesenAbs;
1015
+ if (!roh) { return ''; }
1016
+ if (!k.mehrfach || abs || !k.instanz) { return roh; }
1017
+ return zuPlatzhalter(roh, k.instanz);
1018
+ }
1019
+
1020
+ /* Endet die Nummer eines Ausgangs in den Pfaden? Haeufigste Zahl am
1021
+ Ende eines Abschnitts gewinnt — cmnd.POWER1, stat.POWER1 sagen 1. */
1022
+ function rateInstanz(zeilen, geraetName) {
1023
+ var zaehler = {};
1024
+ zeilen.forEach(function (r) {
1025
+ if (r.lesenAbs) { return; }
1026
+ String(r.lesenRoh).split('.').forEach(function (t) {
1027
+ var m = /^(.*[^0-9])([0-9]{1,2})$/.exec(t);
1028
+ if (m) { zaehler[m[2]] = (zaehler[m[2]] || 0) + 1; }
1029
+ });
1030
+ });
1031
+ var mn = /^(.*[^0-9])([0-9]{1,2})$/.exec(geraetName || '');
1032
+ if (mn) { zaehler[mn[2]] = (zaehler[mn[2]] || 0) + 1; }
1033
+ var beste = '', wie = 0;
1034
+ Object.keys(zaehler).forEach(function (n) {
1035
+ if (zaehler[n] > wie || (zaehler[n] === wie && Number(n) < Number(beste))) {
1036
+ beste = n; wie = zaehler[n];
1037
+ }
1038
+ });
1039
+ return beste;
1040
+ }
1041
+
1042
+ /* Eine Mehrfachvorlage beschreibt genau einen Ausgang — die anderen
1043
+ entstehen daraus. Zeigt der Entwurf noch auf POWER2, POWER3 …, waere
1044
+ die Vorlage an dieses eine Geraet gefesselt. Also einsammeln und
1045
+ sagen, statt still etwas Falsches zu speichern. */
1046
+ function andereAusgaenge(zeilen, instanz) {
1047
+ var raus = { punkte: [], nummern: [] };
1048
+ if (!instanz) { return raus; }
1049
+ zeilen.forEach(function (r) {
1050
+ if (r.weg || r.lesenAbs) { return; }
1051
+ var fremd = null;
1052
+ String(r.lesenRoh).split('.').forEach(function (t) {
1053
+ var m = /^(.*[^0-9])([0-9]{1,2})$/.exec(t);
1054
+ if (m && m[2] !== String(instanz)) { fremd = m[2]; }
1055
+ });
1056
+ if (fremd) {
1057
+ raus.punkte.push(r.name);
1058
+ if (raus.nummern.indexOf(fremd) === -1) { raus.nummern.push(fremd); }
1059
+ }
1060
+ });
1061
+ raus.nummern.sort(function (a, b) { return Number(a) - Number(b); });
1062
+ return raus;
1063
+ }
1064
+
1065
+ function schluesselAus(text) {
1066
+ return String(text || '').toLowerCase()
1067
+ .split('ä').join('ae').split('ö').join('oe').split('ü').join('ue').split('ß').join('ss')
1068
+ .replace(/[^a-z0-9]+/g, '-').replace(/^-+/, '').replace(/-+$/, '') || 'vorlage';
1069
+ }
1070
+
1071
+ /* Der Rang entscheidet nur zwischen Vorlagen derselben Herkunft — der
1072
+ Zuschlag fuer „eigene" liegt in der Bewertung ueber jedem moeglichen
1073
+ Rang. Deshalb wird beim Vergeben auch nur im eigenen Zahlenraum
1074
+ gezaehlt: sonst schoebe eine mitgelieferte Vorlage mit hohem Rang die
1075
+ Nummern des Benutzers mit jedem Update vor sich her, ohne dass sich am
1076
+ Verhalten irgendetwas aenderte. */
1077
+ function eigenerMaxRang() {
1078
+ var m = 0;
1079
+ S.VORLAGEN.forEach(function (v) {
1080
+ if (v._quelle === 'benutzer' && (v.rang || 0) > m) { m = v.rang || 0; }
1081
+ });
1082
+ return m;
1083
+ }
1084
+
1085
+ function freierSchluessel(basis) {
1086
+ var da = {};
1087
+ S.VORLAGEN.forEach(function (v) { da[v.id] = 1; });
1088
+ if (!da[basis]) { return basis; }
1089
+ for (var i = 2; i < 99; i++) { if (!da[basis + '-' + i]) { return basis + '-' + i; } }
1090
+ return basis + '-neu';
1091
+ }
1092
+
1093
+ export function vorlagenVorbereiten(e) {
1094
+ var quelle = null;
1095
+ S.VORLAGEN.forEach(function (v) { if (v.id === e.vorlage) { quelle = v; } });
1096
+ var maxRang = eigenerMaxRang();
1097
+
1098
+ var mehrfach = !!(quelle && quelle.mehrfach);
1099
+ var inst = mehrfach ? e.instanz : null;
1100
+
1101
+ var zeilen = [];
1102
+ e.states.forEach(function (s) {
1103
+ if (!s.n || !s.srcR) { return; }
1104
+ var r = relZu(s.srcR, e.kanal);
1105
+ var w = (s.srcW && s.srcW !== s.srcR) ? relZu(s.srcW, e.kanal) : null;
1106
+ var zv = null;
1107
+ if (quelle && s.ausVorlage) {
1108
+ (quelle.zustaende || []).forEach(function (z) { if (z.name === s.ausVorlage) { zv = z; } });
1109
+ }
1110
+ zeilen.push({
1111
+ s: s,
1112
+ name: s.n,
1113
+ /* Roh heisst: relativ zum Geraet, aber noch mit der konkreten
1114
+ Ausgangsnummer. Der Platzhalter wird erst gesetzt, wenn er
1115
+ gebraucht wird — sonst liesse sich der Haken „mehrere
1116
+ Ausgaenge" nicht mehr umlegen. */
1117
+ lesenRoh: r.p,
1118
+ lesenAbs: r.abs,
1119
+ schreibenRoh: w ? w.p : '',
1120
+ schreibenAbs: w ? w.abs : false,
1121
+ aus: !s.on,
1122
+ /* Vorschlag: was einen Schreibweg hat, traegt das Geraet. Was aus
1123
+ einer Vorlage kam, erbt deren Einstufung. Von Hand ergaenzte
1124
+ Punkte bleiben freiwillig — jeder Pflichtpunkt macht die
1125
+ Vorlage genauer und zugleich sproeder. */
1126
+ pflicht: r.abs ? false : (zv ? !zv.optional : !!w)
1127
+ });
1128
+ });
1129
+
1130
+ var inhalte = [];
1131
+ zeilen.forEach(function (z) {
1132
+ var f = feldAusFormel(z.s.f);
1133
+ if (!f || z.lesenAbs) { return; }
1134
+ var oben = f.split('.')[0];
1135
+ var da = false;
1136
+ inhalte.forEach(function (x) { if (x.punkt === z.lesenRoh && x.feld === oben) { da = true; } });
1137
+ if (!da) { inhalte.push({ punkt: z.lesenRoh, feld: oben, an: false }); }
1138
+ });
1139
+ if (quelle && quelle.erkennung && quelle.erkennung.inhalt) {
1140
+ Object.keys(quelle.erkennung.inhalt).forEach(function (p) {
1141
+ var da = false;
1142
+ inhalte.forEach(function (x) {
1143
+ if (x.punkt === p) { da = true; x.an = true; x.feld = quelle.erkennung.inhalt[p]; }
1144
+ });
1145
+ if (!da) { inhalte.push({ punkt: p, feld: quelle.erkennung.inhalt[p], an: true }); }
1146
+ });
1147
+ }
1148
+
1149
+ var geraetName = (e.kanal || '').split('.').pop();
1150
+ return {
1151
+ e: e,
1152
+ zeilen: zeilen,
1153
+ inhalte: inhalte,
1154
+ kopf: {
1155
+ modus: quelle ? 'update' : 'neu',
1156
+ quelle: quelle,
1157
+ id: quelle ? quelle.id : freierSchluessel(schluesselAus(geraetName)),
1158
+ nameDe: quelle ? textIn(quelle.name, 'de') : geraetName,
1159
+ nameEn: quelle ? textIn(quelle.name, 'en') : geraetName,
1160
+ geraetetyp: e.want || (quelle ? quelle.geraetetyp : '') || '',
1161
+ /* Was die Vorlage schon sagt, gilt; sonst das, was am Geraet
1162
+ eingestellt ist. Wer eine Vorlage aus einem Geraet baut, hat die
1163
+ Funktion dort meist gerade gesetzt - sie noch einmal auswaehlen
1164
+ zu muessen waere Arbeit ohne Gewinn. */
1165
+ funktion: (quelle && quelle.funktion) || kennungZuFunktion(e.funktion) || '',
1166
+ namenshinweis: quelle ? ((quelle.erkennung || {}).namenshinweis || '') : '',
1167
+ rang: quelle ? (quelle.rang || 0) : maxRang + 10,
1168
+ maxRang: maxRang,
1169
+ mehrfach: mehrfach,
1170
+ instanz: mehrfach ? String(inst === null || inst === undefined ? '' : inst)
1171
+ : rateInstanz(zeilen, geraetName),
1172
+ kanalname: (quelle && quelle.kanalname) ? quelle.kanalname : 'POWER%N%',
1173
+ verboten: quelle ? (((quelle.erkennung || {}).verboten) || []).join(', ') : '',
1174
+ geraetName: geraetName
1175
+ }
1176
+ };
1177
+ }
1178
+
1179
+ /* Der Entwurf als Vorlagenobjekt — genau das, was gespeichert wird. */
1180
+ export function baueVorlage(z) {
1181
+ var k = z.kopf;
1182
+ var v = {
1183
+ id: k.id,
1184
+ version: k.modus === 'update' && k.quelle ? (k.quelle.version || 1) + 1 : 1,
1185
+ rang: Number(k.rang) || 0,
1186
+ name: { de: k.nameDe || k.id, en: k.nameEn || k.nameDe || k.id },
1187
+ geraetetyp: k.geraetetyp || undefined,
1188
+ funktion: k.funktion || undefined,
1189
+ erkennung: {}
1190
+ };
1191
+
1192
+ var erf = [];
1193
+ z.zeilen.forEach(function (r) {
1194
+ if (r.weg || !r.pflicht || r.lesenAbs) { return; }
1195
+ var lp = zPfad(r, k, false);
1196
+ var sp = zPfad(r, k, true);
1197
+ if (erf.indexOf(lp) === -1) { erf.push(lp); }
1198
+ if (sp && !r.schreibenAbs && erf.indexOf(sp) === -1) { erf.push(sp); }
1199
+ });
1200
+ v.erkennung.erforderlich = erf;
1201
+
1202
+ var verb = String(k.verboten || '').split(',').map(function (x) { return x.trim(); })
1203
+ .filter(function (x) { return !!x; });
1204
+ /* Ein Punkt kann nicht zugleich Pflicht und verboten sein — so eine
1205
+ Vorlage passt auf gar nichts. Der Widerspruch entsteht leicht: man
1206
+ traegt unten „darf es nicht geben" ein und vergisst, dass derselbe
1207
+ Punkt oben schon „muss da sein" ist. */
1208
+ v.widerspruch = verb.filter(function (x) { return erf.indexOf(x) !== -1; });
1209
+ if (verb.length) { v.erkennung.verboten = verb; }
1210
+
1211
+ var inh = {};
1212
+ z.inhalte.forEach(function (x) { if (x.an) { inh[x.punkt] = x.feld; } });
1213
+ if (Object.keys(inh).length) { v.erkennung.inhalt = inh; }
1214
+ if (k.namenshinweis) { v.erkennung.namenshinweis = k.namenshinweis; }
1215
+
1216
+ if (k.mehrfach && k.instanz) {
1217
+ v.mehrfach = { platzhalter: '%N%' };
1218
+ v.kanalname = k.kanalname || 'POWER%N%';
1219
+ }
1220
+
1221
+ v.zustaende = z.zeilen.filter(function (r) { return !r.weg; }).map(function (r) {
1222
+ var s = r.s;
1223
+ var f = feldAusFormel(s.f);
1224
+ var zu = {
1225
+ name: r.name,
1226
+ rolle: s.role || undefined,
1227
+ typ: s.typ || undefined,
1228
+ einheit: s.unit || undefined,
1229
+ lesen: zPfad(r, k, false)
1230
+ };
1231
+ if (r.lesenAbs) { zu.absolut = true; }
1232
+ if (r.schreibenRoh) { zu.schreiben = zPfad(r, k, true); }
1233
+ if (f) { zu.feld = f; } else if (s.f) { zu.leseformel = s.f; }
1234
+ if (s.fw && r.schreibenRoh) { zu.schreibformel = s.fw; }
1235
+ if (!r.pflicht) { zu.optional = true; }
1236
+ if (r.aus) { zu.vorgabeAus = true; }
1237
+ if (s.caption) { zu.beschriftung = s.caption; }
1238
+ if (s.dec !== undefined && s.dec !== '') { zu.nachkommastellen = Number(s.dec); }
1239
+ if (s.states) { zu.werteliste = s.states; }
1240
+ if (s.hinweis) { zu.hinweis = s.hinweis; }
1241
+ /* Was die Vorlage sonst noch als Lesequelle kannte. Der gerade
1242
+ genommene Weg steht schon in `lesen` und faellt hier weg. */
1243
+ if (s.wege && s.wege.length > 1) {
1244
+ var rest = s.wege.filter(function (w) { return w.punkt !== zu.lesen; })
1245
+ .map(function (w) {
1246
+ var o = { punkt: w.punkt };
1247
+ if (w.feld) { o.feld = w.feld; }
1248
+ if (w.formel) { o.formel = w.formel; }
1249
+ return o;
1250
+ });
1251
+ if (rest.length) { zu.lesenSonst = (rest.length === 1) ? rest[0] : rest; }
1252
+ }
1253
+ return zu;
1254
+ });
1255
+
1256
+ /* Das Rollenwissen: alles, was am Geraet liegt und keine Zeile der
1257
+ Vorlage ist, wandert als Pfad→Rolle mit. Der Vorschlag ignoriert
1258
+ es; die Ansicht „Alle Datenpunkte des Geraets" liest daraus die
1259
+ Rollen, die hm-rpc am Geraet leer laesst. Rangfolge je Punkt: was
1260
+ die aktuelle Zeile sagt (Handarbeit im Entwurf), dann das
1261
+ gepflegte Wissen der Ursprungsvorlage, zuletzt die Geraeterolle.
1262
+ Ohne Rolle kein Eintrag - leeres Wissen hilft niemandem. */
1263
+ var zeilenQuellen = {};
1264
+ z.zeilen.forEach(function (r) {
1265
+ if (r.weg) { return; }
1266
+ if (r.lesenRoh && !r.lesenAbs) { zeilenQuellen[r.lesenRoh] = true; }
1267
+ if (r.schreibenRoh && !r.schreibenAbs) { zeilenQuellen[r.schreibenRoh] = true; }
1268
+ });
1269
+ var altesWissen = (k.modus === 'update' && k.quelle && k.quelle.weiterePunkte) || {};
1270
+ var wissen = {};
1271
+ kindZustaende(z.e.kanal).forEach(function (id) {
1272
+ var rel = relZu(id, z.e.kanal);
1273
+ if (rel.abs || zeilenQuellen[rel.p]) { return; }
1274
+ var zeilenRolle = '';
1275
+ z.e.states.forEach(function (s0) {
1276
+ if (!zeilenRolle && s0.srcR === id && s0.role) { zeilenRolle = s0.role; }
1277
+ });
1278
+ var o0 = S.objects[id];
1279
+ var rolle = zeilenRolle || altesWissen[rel.p] ||
1280
+ ((o0 && o0.common && o0.common.role) || '');
1281
+ if (rolle) { wissen[rel.p] = rolle; }
1282
+ });
1283
+ /* Gepflegtes Wissen zu Punkten, die dieses eine Geraet gerade nicht
1284
+ hat, bleibt erhalten - andere Geraete des Typs haben sie. */
1285
+ Object.keys(altesWissen).forEach(function (pf) {
1286
+ if (!zeilenQuellen[pf] && !wissen[pf]) { wissen[pf] = altesWissen[pf]; }
1287
+ });
1288
+ if (Object.keys(wissen).length) { v.weiterePunkte = wissen; }
1289
+
1290
+ /* Was die Ursprungsvorlage sonst noch mitbringt — etwa
1291
+ nebenwirkungen — bleibt erhalten. Sonst fiele beim Aktualisieren
1292
+ still unter den Tisch, was der Dialog gar nicht anzeigt. */
1293
+ var bekannt = { id: 1, version: 1, rang: 1, name: 1, geraetetyp: 1,
1294
+ funktion: 1, erkennung: 1, mehrfach: 1, kanalname: 1,
1295
+ zustaende: 1, weiterePunkte: 1 };
1296
+ if (k.modus === 'update' && k.quelle) {
1297
+ Object.keys(k.quelle).forEach(function (feld) {
1298
+ if (feld.charAt(0) === '_' || bekannt[feld]) { return; }
1299
+ v[feld] = k.quelle[feld];
1300
+ });
1301
+ }
1302
+
1303
+ return JSON.parse(JSON.stringify(v)); /* undefined weg */
1304
+ }
1305
+
1306
+ /* Kandidaten fuer den Probelauf. Nicht jeder Knoten im Baum — nur die,
1307
+ unter denen der erste Pflichtpunkt ueberhaupt vorkommt. Sonst muesste
1308
+ hatPunkt fuer tausende Knoten die Kleinschreibkarte aufbauen, und das
1309
+ dauert auf einer grossen Anlage sichtbar lange. */
1310
+ function kandidatenFuer(v) {
1311
+ var erk = v.erkennung || {};
1312
+ var erf = erk.erforderlich || [];
1313
+ if (!erf.length) { return []; }
1314
+ var ph = v.mehrfach ? (v.mehrfach.platzhalter || '%N%') : null;
1315
+ var raus = {};
1316
+
1317
+ function sammle(pfad) {
1318
+ var teile = ph ? String(pfad).split(ph) : [String(pfad)];
1319
+ var vorn = teile[0];
1320
+ if (vorn) {
1321
+ var suche = ('.' + vorn).toLowerCase();
1322
+ S.keysSorted.forEach(function (id) {
1323
+ if (id.indexOf('alias.') === 0) { return; }
1324
+ if (!S.objects[id] || S.objects[id].type !== 'state') { return; }
1325
+ var i = id.toLowerCase().indexOf(suche);
1326
+ if (i < 1) { return; }
1327
+ var kanal = id.slice(0, i);
1328
+ if (kanal.split('.').length >= 3) { raus[kanal] = 1; }
1329
+ });
1330
+ return;
1331
+ }
1332
+
1333
+ /* Der Platzhalter kann auch ganz vorn stehen. Eine Tasmota nennt
1334
+ ihre Ausgaenge cmnd.POWER%N% — da bleibt ein Vorspann uebrig, an
1335
+ dem sich der Kanal abschneiden laesst. Ein Homematic-Doppelaktor
1336
+ nennt sie %N%.STATE, und dann ist der Vorspann leer: der Kanal
1337
+ ist die Nummer selbst. Frueher stieg die Suche hier aus und die
1338
+ Vorlage fand kein einziges Geraet. Jetzt laeuft sie ueber den
1339
+ Nachspann und nimmt das Segment davor als Ausgangsnummer. */
1340
+ var nach = (teile[1] || '').toLowerCase();
1341
+ if (!nach) { return; }
1342
+ S.keysSorted.forEach(function (id) {
1343
+ if (id.indexOf('alias.') === 0) { return; }
1344
+ if (!S.objects[id] || S.objects[id].type !== 'state') { return; }
1345
+ if (id.toLowerCase().slice(-nach.length) !== nach) { return; }
1346
+ var t = id.slice(0, id.length - nach.length).split('.');
1347
+ t.pop();
1348
+ if (t.length >= 3) { raus[t.join('.')] = 1; }
1349
+ });
1350
+ }
1351
+
1352
+ sammle(erf[0]);
1353
+ /* Auch ueber die Inhaltspruefung suchen. Ein Geraet, das seine
1354
+ Ausgaenge nur in tele/STATE meldet und noch keinen cmnd-Punkt hat,
1355
+ waere sonst kein Kandidat — der Probelauf einer Mehrfach-Vorlage
1356
+ fand ein Geraet statt vier. Was am Ende wirklich passt, entscheidet
1357
+ `pruefeVorlage`; hier geht es nur darum, niemanden zu uebersehen. */
1358
+ Object.keys(erk.inhalt || {}).forEach(sammle);
1359
+
1360
+ return Object.keys(raus).sort();
1361
+ }
1362
+
1363
+ /* Der Probelauf braucht Werte, nicht nur Objekte.
1364
+ `pruefeVorlage` prueft auch `erkennung.inhalt` und schaut dafuer in
1365
+ den Wert des Punktes — steht dort nichts, gilt die Vorlage als
1366
+ unpassend. Geladen werden die Werte aber nur fuer den gewaehlten
1367
+ Kanal, und im Vorlagen-Tab ist keiner gewaehlt. Also meldete jede
1368
+ Vorlage mit `inhalt` „0 Geraete getroffen", auch die, die im
1369
+ Quellen-Tab zwanzig Geraete erkennt.
1370
+
1371
+ Hier werden die fehlenden Werte einmal nachgeladen und danach neu
1372
+ gezeichnet. Was einmal vergeblich abgefragt wurde, wird nicht wieder
1373
+ abgefragt — sonst laedt ein Punkt ohne Wert endlos. */
1374
+ var probeGeholt = {};
1375
+
1376
+ function probeWerteHolen(v, danach) {
1377
+ var erk = v.erkennung || {};
1378
+ if (!erk.inhalt) { return false; }
1379
+ var ph = v.mehrfach ? (v.mehrfach.platzhalter || '%N%') : null;
1380
+ var fehlt = [];
1381
+ kandidatenFuer(v).forEach(function (kanal) {
1382
+ Object.keys(erk.inhalt).forEach(function (p0) {
1383
+ var pfad = ph ? String(p0).split(ph).join('1') : p0;
1384
+ var id = hatPunkt(kanal, pfad);
1385
+ if (id && S.werte[id] === undefined && !probeGeholt[id]) { fehlt.push(id); }
1386
+ });
1387
+ });
1388
+ if (!fehlt.length) { return false; }
1389
+ var offen = fehlt.length;
1390
+ fehlt.forEach(function (id) {
1391
+ probeGeholt[id] = 1;
1392
+ socket.emit('getState', id, function (err, st) {
1393
+ if (!err && st) { S.werte[id] = st; }
1394
+ if (--offen === 0) { danach(); }
1395
+ });
1396
+ });
1397
+ return true;
1398
+ }
1399
+
1400
+ /* Was faengt diese Vorlage, und wer gewinnt dort heute? */
1401
+ export function probelauf(vorlage, alsEigene) {
1402
+ /* Nie am Original arbeiten — sonst stuende hinterher _quelle im
1403
+ gespeicherten JSON. */
1404
+ var neu = JSON.parse(JSON.stringify(vorlage));
1405
+ /* Nur rechnen, was auch zutrifft. Eine eigene Vorlage bekommt in der
1406
+ Bewertung 1000 Punkte Vorsprung; setzte man das hier immer, meldete
1407
+ das Blatt einer mitgelieferten Vorlage Siege, die sie nie erringt —
1408
+ Steckdose 18 und Lampe 23 „gewonnen" bei 26 Geraeten, obwohl je
1409
+ Geraet nur eine gewinnen kann. Im Dialog „Als Vorlage speichern"
1410
+ stimmt es dagegen: dort wird sie gerade zu einer eigenen. */
1411
+ if (alsEigene) { neu._quelle = 'benutzer'; }
1412
+ var raus = [];
1413
+ kandidatenFuer(neu).forEach(function (p) {
1414
+ var t = pruefeVorlage(neu, p);
1415
+ if (!t.passt) { return; }
1416
+
1417
+ /* Wer gewinnt hier heute, mit dem Bestand wie er ist? Beim
1418
+ Aktualisieren ist das oft die Vorlage selbst — dann wechselt
1419
+ nichts, auch wenn sie den Vergleich gegen alle anderen gewinnt. */
1420
+ var jetzt = null, andere = null;
1421
+ S.VORLAGEN.forEach(function (v) {
1422
+ var x = pruefeVorlage(v, p);
1423
+ if (!x.passt) { return; }
1424
+ if (!jetzt || x.punkte > jetzt.punkte) { jetzt = x; }
1425
+ if (v.id !== neu.id && (!andere || x.punkte > andere.punkte)) { andere = x; }
1426
+ });
1427
+ raus.push({
1428
+ id: p, eigen: t, gegner: andere,
1429
+ gewinnt: !andere || t.punkte > andere.punkte,
1430
+ wechsel: !!(jetzt && jetzt.vorlage.id !== neu.id &&
1431
+ (!andere || t.punkte > andere.punkte))
1432
+ });
1433
+ });
1434
+ return raus;
1435
+ }
1436
+
1437
+ export function tplKnopf(an) {
1438
+ var b = $('#btn-tpl');
1439
+ if (b) {
1440
+ b.disabled = !an;
1441
+ b.title = an ? '' : tr('tpls.pickDeviceFirst');
1442
+ }
1443
+ }
1444
+
1445
+ export function zeigeVorlagenDialog() {
1446
+ if (!S.entwurf || !S.current) { return; }
1447
+ S.tplZ = vorlagenVorbereiten(S.entwurf);
1448
+ var b = $('#btn-tpl-save');
1449
+ if (b) { b.hidden = false; b.disabled = false; }
1450
+ zeichneVorlagenDialog();
1451
+ $('#dlg-tpl').showModal();
1452
+ }
1453
+
1454
+ function tplFeld(label, wert, aendern, breit) {
1455
+ var l = el('label', 'feld');
1456
+ l.appendChild(el('span', 'feldlabel', label));
1457
+ var i = document.createElement('input');
1458
+ i.className = 'tx';
1459
+ i.type = 'text';
1460
+ i.value = wert === undefined || wert === null ? '' : String(wert);
1461
+ if (breit) { i.style.minWidth = breit; }
1462
+ i.addEventListener('input', function () { aendern(i.value); });
1463
+ i.addEventListener('change', function () { aendern(i.value); zeichneVorlagenDialog(); });
1464
+ l.appendChild(i);
1465
+ return l;
1466
+ }
1467
+
1468
+ export function zeichneVorlagenDialog() {
1469
+ var z = S.tplZ;
1470
+ if (!z) { return; }
1471
+ var k = z.kopf;
1472
+ var body = $('#tpl-body');
1473
+ body.textContent = '';
1474
+
1475
+ /* --- aktualisieren oder neu --- */
1476
+ if (k.quelle) {
1477
+ var wahl = el('div', 'card');
1478
+ wahl.style.marginBottom = '11px';
1479
+ var wh = el('div', 'ch');
1480
+ wh.appendChild(el('span', 'typ', tr('tpls.whatToDo')));
1481
+ wahl.appendChild(wh);
1482
+ var wb = el('div');
1483
+ wb.style.padding = '9px 12px 11px';
1484
+ [['update', tr('tpls.updateIt', textIn(k.quelle.name, sprache), (k.quelle.version || 1) + 1),
1485
+ k.quelle._quelle === 'paket' ? tr('tpls.shadowsPackage') : ''],
1486
+ ['neu', tr('tpls.saveAsNew'), tr('tpls.saveAsNewHint')]].forEach(function (w) {
1487
+ var r = el('label');
1488
+ r.style.display = 'block';
1489
+ r.style.margin = '3px 0';
1490
+ r.style.fontSize = '12.5px';
1491
+ var rb = document.createElement('input');
1492
+ rb.type = 'radio';
1493
+ rb.name = 'tplmodus';
1494
+ rb.checked = k.modus === w[0];
1495
+ rb.addEventListener('change', function () {
1496
+ if (!rb.checked) { return; }
1497
+ k.modus = w[0];
1498
+ if (w[0] === 'neu') {
1499
+ k.id = freierSchluessel(schluesselAus(k.nameDe || k.geraetName));
1500
+ k.rang = k.maxRang + 10;
1501
+ } else {
1502
+ k.id = k.quelle.id;
1503
+ k.rang = k.quelle.rang || 0;
1504
+ }
1505
+ zeichneVorlagenDialog();
1506
+ });
1507
+ r.appendChild(rb);
1508
+ r.appendChild(document.createTextNode(' ' + w[1]));
1509
+ if (w[2]) {
1510
+ var hn = el('span', 'chip mut', w[2]);
1511
+ hn.style.marginLeft = '6px';
1512
+ r.appendChild(hn);
1513
+ }
1514
+ wb.appendChild(r);
1515
+ });
1516
+ wahl.appendChild(wb);
1517
+ body.appendChild(wahl);
1518
+ }
1519
+
1520
+ /* --- Kopfdaten --- */
1521
+ var kk = el('div', 'card');
1522
+ kk.style.marginBottom = '11px';
1523
+ var kh = el('div', 'ch');
1524
+ kh.appendChild(el('span', 'typ', tr('tpls.head')));
1525
+ kh.appendChild(el('span', 'chip mut', k.id));
1526
+ kk.appendChild(kh);
1527
+ var kb = el('div');
1528
+ kb.style.padding = '9px 12px 11px';
1529
+ kb.style.display = 'flex';
1530
+ kb.style.flexWrap = 'wrap';
1531
+ kb.style.gap = '9px 14px';
1532
+
1533
+ kb.appendChild(tplFeld(tr('tpls.nameDe'), k.nameDe, function (x) { k.nameDe = x; }, '150px'));
1534
+ kb.appendChild(tplFeld(tr('tpls.nameEn'), k.nameEn, function (x) { k.nameEn = x; }, '150px'));
1535
+ kb.appendChild(tplFeld(tr('tpls.id'), k.id, function (x) { k.id = schluesselAus(x); }, '150px'));
1536
+
1537
+ var lt = el('label', 'feld');
1538
+ lt.appendChild(el('span', 'feldlabel', tr('tpls.deviceType')));
1539
+ var st = el('select', 'tx');
1540
+ st.appendChild(opt('', tr('tpls.noType')));
1541
+ (D ? Object.keys(D.patterns).sort() : []).forEach(function (m) {
1542
+ var o = opt(m, musterZeile(m));
1543
+ if (m === k.geraetetyp) { o.selected = true; }
1544
+ st.appendChild(o);
1545
+ });
1546
+ st.addEventListener('change', function () { k.geraetetyp = st.value; zeichneVorlagenDialog(); });
1547
+ lt.appendChild(st);
1548
+ kb.appendChild(lt);
1549
+
1550
+ /* Die Funktion gehoert in die Vorlage, nicht nur ans einzelne Geraet.
1551
+
1552
+ Der Geraetetyp sagt, *wie* etwas gebaut ist - `socket` heisst
1553
+ geschaltet, mehr nicht. Ob eine Steckdose, eine Waschmaschine oder
1554
+ eine Druckerleiste daran haengt, weiss allein die Vorlage. Sie traegt
1555
+ die Funktion, und der naechste Alias aus dieser Vorlage bekommt sie
1556
+ vorgeschlagen. Bisher liess sie sich nur nachtraeglich im
1557
+ Vorlagenblatt setzen - beim Speichern gab es das Feld nicht. */
1558
+ var lf2 = el('label', 'feld');
1559
+ lf2.appendChild(el('span', 'feldlabel', tr('tpls.function')));
1560
+ var sf2 = el('select', 'tx');
1561
+ sf2.appendChild(opt('', tr('tpls.noFunction')));
1562
+ funktionsAuswahl(k.funktion).forEach(function (x) {
1563
+ var o = opt(x.wert, x.text);
1564
+ if (x.gewaehlt) { o.selected = true; }
1565
+ sf2.appendChild(o);
1566
+ });
1567
+ sf2.addEventListener('change', function () { k.funktion = sf2.value; });
1568
+ lf2.appendChild(sf2);
1569
+ if (katalogFehlt) { lf2.appendChild(el('span', 'hint', tr('catalog.missing'))); }
1570
+ kb.appendChild(lf2);
1571
+
1572
+ kb.appendChild(tplFeld(tr('tpls.rank'), k.rang, function (x) { k.rang = x; }, '60px'));
1573
+ var rh2 = el('div', 'hint', tr('tv.rankOwn'));
1574
+ rh2.style.flexBasis = '100%';
1575
+ kb.appendChild(rh2);
1576
+ kk.appendChild(kb);
1577
+
1578
+ var nh = el('div');
1579
+ nh.style.padding = '0 12px 11px';
1580
+ var nf = tplFeld(tr('tpls.nameHint'), k.namenshinweis, function (x) { k.namenshinweis = x; }, '260px');
1581
+ nh.appendChild(nf);
1582
+ var nt = el('div', 'hint', tr('tpls.nameHintHelp', k.geraetName));
1583
+ nt.style.marginTop = '4px';
1584
+ nh.appendChild(nt);
1585
+ kk.appendChild(nh);
1586
+
1587
+ /* --- mehrere Ausgaenge --- */
1588
+ var mf = el('div');
1589
+ mf.style.padding = '0 12px 12px';
1590
+ var ml = el('label');
1591
+ ml.style.display = 'block';
1592
+ ml.style.fontSize = '12.5px';
1593
+ var mcb = document.createElement('input');
1594
+ mcb.type = 'checkbox';
1595
+ mcb.checked = !!k.mehrfach;
1596
+ mcb.addEventListener('change', function () {
1597
+ k.mehrfach = mcb.checked;
1598
+ if (k.mehrfach && !k.instanz) { k.instanz = rateInstanz(z.zeilen, k.geraetName); }
1599
+ zeichneVorlagenDialog();
1600
+ });
1601
+ ml.appendChild(mcb);
1602
+ ml.appendChild(document.createTextNode(' ' + tr('tpls.multiple')));
1603
+ mf.appendChild(ml);
1604
+
1605
+ if (k.mehrfach) {
1606
+ var mz = el('div');
1607
+ mz.style.display = 'flex';
1608
+ mz.style.flexWrap = 'wrap';
1609
+ mz.style.gap = '9px 14px';
1610
+ mz.style.marginTop = '6px';
1611
+ mz.appendChild(tplFeld(tr('tpls.whichNumber'), k.instanz,
1612
+ function (x) { k.instanz = String(x).trim(); }, '60px'));
1613
+ mz.appendChild(tplFeld(tr('tpls.channelName'), k.kanalname,
1614
+ function (x) { k.kanalname = x; }, '140px'));
1615
+ mf.appendChild(mz);
1616
+ mf.appendChild(el('div', 'hint',
1617
+ k.instanz ? tr('tpls.multipleHelp', k.instanz) : tr('tpls.multipleNoNumber')));
1618
+ var fremd = andereAusgaenge(z.zeilen, k.instanz);
1619
+ if (fremd.punkte.length) {
1620
+ var fw2 = el('div', 'aside w');
1621
+ fw2.style.margin = '9px 0 0';
1622
+ fw2.appendChild(el('b', null, tr('tpls.otherOutputs', fremd.punkte.length, fremd.nummern.join(', '))));
1623
+ fw2.appendChild(document.createTextNode(' ' + tr('tpls.otherOutputsText')));
1624
+ /* Nicht nur schimpfen, sondern es koennen: die fremden Zeilen
1625
+ fliegen aus der Vorlage — das Geraet bleibt unangetastet. */
1626
+ var fb = el('button', 'btn schmal', tr('tpls.dropOthers'));
1627
+ fb.style.marginTop = '8px';
1628
+ fb.addEventListener('click', function () {
1629
+ z.zeilen.forEach(function (r) {
1630
+ if (fremd.punkte.indexOf(r.name) > -1) { r.weg = true; }
1631
+ });
1632
+ zeichneVorlagenDialog();
1633
+ });
1634
+ fw2.appendChild(el('div', null, '')).appendChild(fb);
1635
+ mf.appendChild(fw2);
1636
+ }
1637
+ var weg = z.zeilen.filter(function (r) { return r.weg; });
1638
+ if (weg.length) {
1639
+ var wz = el('div', 'hint');
1640
+ wz.style.marginTop = '7px';
1641
+ wz.appendChild(document.createTextNode(tr('tpls.droppedCount', weg.length) + ' '));
1642
+ var zb = el('button', 'btn schmal', tr('tpls.undrop'));
1643
+ zb.addEventListener('click', function () {
1644
+ z.zeilen.forEach(function (r) { r.weg = false; });
1645
+ zeichneVorlagenDialog();
1646
+ });
1647
+ wz.appendChild(zb);
1648
+ mf.appendChild(wz);
1649
+ }
1650
+ } else {
1651
+ mf.appendChild(el('div', 'hint', tr('tpls.multipleOff')));
1652
+ }
1653
+ kk.appendChild(mf);
1654
+
1655
+ /* --- Punkte, die es nicht geben darf --- */
1656
+ var vf = el('div');
1657
+ vf.style.padding = '0 12px 12px';
1658
+ vf.appendChild(tplFeld(tr('tv.forbidden'), k.verboten, function (x) { k.verboten = x; }, '280px'));
1659
+ vf.appendChild(el('div', 'hint', tr('tv.forbiddenHelp')));
1660
+ kk.appendChild(vf);
1661
+
1662
+ body.appendChild(kk);
1663
+
1664
+ /* --- Zustaende --- */
1665
+ var zk = el('div', 'card');
1666
+ zk.style.marginBottom = '11px';
1667
+ var zh = el('div', 'ch');
1668
+ var drin = z.zeilen.filter(function (r) { return !r.weg; });
1669
+ zh.appendChild(el('span', 'typ', tr('tpls.states')));
1670
+ zh.appendChild(el('span', 'chip mut', tr('tpls.statesCount', drin.length)));
1671
+ zk.appendChild(zh);
1672
+
1673
+ /* Drei verschiedene Fragen je Zeile, deshalb drei Spalten. Frueher
1674
+ stand nur ein Haken links, und der bedeutete "muss vorhanden sein" —
1675
+ an dieser Stelle liest aber jeder "ist ausgewaehlt", weil er genau
1676
+ das in jeder anderen Liste dieses Adapters bedeutet. */
1677
+ var erkl = el('div', 'hint');
1678
+ erkl.style.padding = '8px 12px 0';
1679
+ erkl.appendChild(document.createTextNode(tr('tpls.statesHelp')));
1680
+ zk.appendChild(erkl);
1681
+
1682
+ var abgewaehlt = z.zeilen.filter(function (r) { return !r.weg && r.aus; });
1683
+ if (abgewaehlt.length) {
1684
+ var az = el('div', 'hint');
1685
+ az.style.padding = '6px 12px 0';
1686
+ az.appendChild(document.createTextNode(tr('tpls.someOff', abgewaehlt.length) + ' '));
1687
+ var ab = el('button', 'btn schmal', tr('tpls.dropOff'));
1688
+ ab.addEventListener('click', function () {
1689
+ abgewaehlt.forEach(function (r) { r.weg = true; r.pflicht = false; });
1690
+ zeichneVorlagenDialog();
1691
+ });
1692
+ az.appendChild(ab);
1693
+ zk.appendChild(az);
1694
+ }
1695
+
1696
+ var kopfz = el('div', 'slot vzeile vkopf');
1697
+ [tr('tpls.colInTemplate'), tr('tpls.colDefault'), tr('tpls.colRequired'),
1698
+ tr('tpls.colName'), tr('tpls.colPath')]
1699
+ .forEach(function (t) { kopfz.appendChild(el('span', null, t)); });
1700
+ zk.appendChild(kopfz);
1701
+
1702
+ var absDa = false;
1703
+ z.zeilen.forEach(function (r) {
1704
+ if (!r.weg && r.lesenAbs) { absDa = true; }
1705
+ var row = el('div', 'slot vzeile');
1706
+ if (r.weg) { row.style.opacity = '.45'; }
1707
+
1708
+ var haken = function (an, gesperrt, titel, tut) {
1709
+ var w = el('span');
1710
+ var c = document.createElement('input');
1711
+ c.type = 'checkbox';
1712
+ c.checked = an;
1713
+ c.disabled = !!gesperrt;
1714
+ c.title = titel;
1715
+ c.addEventListener('change', function () { tut(c.checked); });
1716
+ w.appendChild(c);
1717
+ return w;
1718
+ };
1719
+
1720
+ /* 1. Kommt der Punkt ueberhaupt in die Vorlage? */
1721
+ row.appendChild(haken(!r.weg, false, tr('tpls.colInTemplate'), function (an) {
1722
+ r.weg = !an;
1723
+ if (r.weg) { r.pflicht = false; }
1724
+ zeichneVorlagenDialog();
1725
+ }));
1726
+
1727
+ /* 2. Ist er beim naechsten Geraet vorgehakt? Kommt vom Haken am
1728
+ Geraet, laesst sich hier aber noch drehen. */
1729
+ row.appendChild(haken(!r.aus, r.weg, tr('tpls.colDefault'), function (an) {
1730
+ r.aus = !an;
1731
+ zeichneVorlagenDialog();
1732
+ }));
1733
+
1734
+ /* 3. Muss er da sein, damit die Vorlage ueberhaupt greift? */
1735
+ row.appendChild(haken(r.pflicht, r.lesenAbs || r.weg, tr('tpls.colRequired'), function (an) {
1736
+ r.pflicht = an;
1737
+ zeichneVorlagenDialog();
1738
+ }));
1739
+
1740
+ row.appendChild(el('span', 'sn', r.name));
1741
+
1742
+ var pw = el('span', 'rx pf');
1743
+ pw.style.fontFamily = 'var(--mono)';
1744
+ pw.title = zPfad(r, k, false) + (r.schreibenRoh ? ' → ' + zPfad(r, k, true) : '');
1745
+ pw.appendChild(document.createTextNode(zPfad(r, k, false)));
1746
+ if (r.schreibenRoh) { pw.appendChild(el('span', 'chip mut', '→ ' + zPfad(r, k, true))); }
1747
+ if (r.lesenAbs) { pw.appendChild(el('span', 'chip warn', tr('tpls.absolute'))); }
1748
+ row.appendChild(pw);
1749
+
1750
+ zk.appendChild(row);
1751
+ });
1752
+ body.appendChild(zk);
1753
+
1754
+ if (absDa) {
1755
+ var aw = el('div', 'aside w');
1756
+ aw.style.marginBottom = '11px';
1757
+ aw.appendChild(el('b', null, tr('tpls.absoluteTitle')));
1758
+ aw.appendChild(document.createTextNode(tr('tpls.absoluteText')));
1759
+ body.appendChild(aw);
1760
+ }
1761
+
1762
+ /* --- Inhaltspruefungen --- */
1763
+ if (z.inhalte.length) {
1764
+ var ik = el('div', 'card');
1765
+ ik.style.marginBottom = '11px';
1766
+ var ih = el('div', 'ch');
1767
+ ih.appendChild(el('span', 'typ', tr('tpls.content')));
1768
+ ik.appendChild(ih);
1769
+ var ib = el('div');
1770
+ ib.style.padding = '9px 12px 11px';
1771
+ z.inhalte.forEach(function (x) {
1772
+ var r = el('label');
1773
+ r.style.display = 'block';
1774
+ r.style.margin = '3px 0';
1775
+ r.style.fontSize = '12.5px';
1776
+ var cb = document.createElement('input');
1777
+ cb.type = 'checkbox';
1778
+ cb.checked = x.an;
1779
+ cb.addEventListener('change', function () { x.an = cb.checked; zeichneVorlagenDialog(); });
1780
+ r.appendChild(cb);
1781
+ var t = el('span', null, ' ' + tr('tpls.contentRow', x.punkt, x.feld));
1782
+ t.style.fontFamily = 'var(--mono)';
1783
+ r.appendChild(t);
1784
+ ib.appendChild(r);
1785
+ });
1786
+ ib.appendChild(el('div', 'hint', tr('tpls.contentHelp')));
1787
+ ik.appendChild(ib);
1788
+ body.appendChild(ik);
1789
+ }
1790
+
1791
+ /* --- Probelauf --- */
1792
+ var neu = baueVorlage(z);
1793
+ var pk = el('div', 'card');
1794
+ pk.style.marginBottom = '11px';
1795
+ var ph = el('div', 'ch');
1796
+ ph.appendChild(el('span', 'typ', tr('tpls.tryout')));
1797
+ var treffer = [];
1798
+ var fehlerErk = !neu.erkennung.erforderlich.length;
1799
+ if (!fehlerErk) {
1800
+ probeWerteHolen(neu, zeichneVorlagenDialog);
1801
+ treffer = probelauf(neu, true);
1802
+ }
1803
+ var gew = treffer.filter(function (t) { return t.gewinnt; }).length;
1804
+ var wech = treffer.filter(function (t) { return t.wechsel; }).length;
1805
+ ph.appendChild(el('span', 'chip ' + (gew ? 'ok' : 'warn'), tr('tpls.hits', treffer.length, gew)));
1806
+ if (wech) { ph.appendChild(el('span', 'chip warn', tr('tpls.takesOver', wech))); }
1807
+ pk.appendChild(ph);
1808
+ var pb = el('div');
1809
+ pb.style.padding = '9px 12px 11px';
1810
+ if (fehlerErk) {
1811
+ pb.appendChild(el('div', 'hint', tr('tpls.noRequired')));
1812
+ } else if (!treffer.length) {
1813
+ pb.appendChild(el('div', 'hint', tr('tpls.noHits')));
1814
+ } else {
1815
+ treffer.slice(0, 40).forEach(function (t) {
1816
+ var r = el('div', 'slot');
1817
+ r.style.gridTemplateColumns = '1fr 150px 130px';
1818
+ var idl = el('span', 'sn', t.id);
1819
+ idl.style.fontFamily = 'var(--mono)';
1820
+ if (t.id === (z.e.kanal || '')) { idl.style.fontWeight = '700'; }
1821
+ r.appendChild(idl);
1822
+ r.appendChild(el('span', 'rx', tr('tpls.evidence', t.eigen.belege)));
1823
+ var w = el('span', 'chip ' + (t.gewinnt ? 'ok' : 'bad'),
1824
+ t.gewinnt ? (t.gegner ? tr('tpls.beats', textIn(t.gegner.vorlage.name, sprache)) : tr('tpls.onlyOne'))
1825
+ : tr('tpls.losesTo', textIn(t.gegner.vorlage.name, sprache)));
1826
+ r.appendChild(w);
1827
+ pb.appendChild(r);
1828
+ });
1829
+ if (treffer.length > 40) {
1830
+ pb.appendChild(el('div', 'hint', tr('tpls.andMore', treffer.length - 40)));
1831
+ }
1832
+ }
1833
+ pk.appendChild(pb);
1834
+ body.appendChild(pk);
1835
+
1836
+ /* --- das fertige JSON --- */
1837
+ var jk = el('div', 'card');
1838
+ var jh = el('div', 'ch');
1839
+ jh.appendChild(el('span', 'typ', tr('tpls.json')));
1840
+ jh.appendChild(el('span', 'chip mut', 'v' + neu.version));
1841
+ jk.appendChild(jh);
1842
+ var pre = el('div', 'raw');
1843
+ pre.style.margin = '9px 12px 11px';
1844
+ pre.style.whiteSpace = 'pre';
1845
+ pre.style.maxHeight = 'none';
1846
+ pre.textContent = JSON.stringify(neu, null, 2);
1847
+ jk.appendChild(pre);
1848
+ body.appendChild(jk);
1849
+
1850
+ var b = $('#btn-tpl-save');
1851
+ if (b) {
1852
+ b.disabled = fehlerErk || !k.id ||
1853
+ !z.zeilen.filter(function (r) { return !r.weg; }).length;
1854
+ b.textContent = k.modus === 'update' ? tr('tpls.saveUpdate') : tr('tpls.saveNew');
1855
+ }
1856
+ var tt = $('#tpl-titel');
1857
+ if (tt) { tt.textContent = tr('tpls.title', z.e.kanal.split('.').pop()); }
1858
+ }
1859
+
1860
+ export function speichereVorlageAusDialog() {
1861
+ if (!S.tplZ) { return; }
1862
+ var neu = baueVorlage(S.tplZ);
1863
+
1864
+ /* Widerspricht sie sich, wird nicht gespeichert — eine Vorlage, die
1865
+ auf nichts passen kann, hilft niemandem und ist spaeter schwer zu
1866
+ durchschauen. */
1867
+ if (neu.widerspruch && neu.widerspruch.length) {
1868
+ var body0 = $('#tpl-body');
1869
+ var alt0 = body0.querySelector('.widerspruch');
1870
+ if (alt0) { alt0.remove(); }
1871
+ var w0 = el('div', 'aside w widerspruch');
1872
+ w0.appendChild(el('b', null, tr('tpls.contradiction')));
1873
+ w0.appendChild(document.createTextNode(' ' + tr('tpls.contradictionText',
1874
+ neu.widerspruch.join(', '))));
1875
+ body0.insertBefore(w0, body0.firstChild);
1876
+ body0.scrollTop = 0;
1877
+ return;
1878
+ }
1879
+ delete neu.widerspruch;
1880
+
1881
+ var b = $('#btn-tpl-save');
1882
+ if (b) { b.disabled = true; b.textContent = tr('tpls.saving'); }
1883
+
1884
+ aendereVorlagen(neu.id, neu, function (err) {
1885
+ var body = $('#tpl-body');
1886
+ if (err) {
1887
+ body.textContent = '';
1888
+ var w = el('div', 'aside w');
1889
+ w.appendChild(el('b', null, tr('tpls.failed')));
1890
+ w.appendChild(document.createTextNode(' ' + err));
1891
+ body.appendChild(w);
1892
+ if (b) { b.disabled = false; b.textContent = tr('tpls.saveNew'); }
1893
+ return;
1894
+ }
1895
+ ladeVorlagen(function () {
1896
+ setzeMeta();
1897
+ body.textContent = '';
1898
+ var m = el('div');
1899
+ m.style.fontSize = '13px';
1900
+ m.appendChild(el('b', null, tr('tpls.saved', neu.id, neu.version)));
1901
+ m.appendChild(el('div', null, tr('tpls.savedWhere', INSTANZ_ID)));
1902
+ body.appendChild(m);
1903
+ if (b) { b.hidden = true; }
1904
+ if (S.current) { zeichneErgebnis(); }
1905
+ });
1906
+ });
1907
+ }