iobroker.alias-workbench 0.8.3 → 0.9.7

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.
package/admin/js/werte.js CHANGED
@@ -52,10 +52,34 @@ export function feldWert(obj, pfad) {
52
52
  }
53
53
 
54
54
 
55
+ /* Alle Kennungen mit diesem Vorspann — ohne die Gesamtliste zu lesen.
56
+
57
+ `S.keysSorted` ist sortiert, die gesuchten Eintraege liegen also am
58
+ Stueck. Vorher ging jeder dieser Aufrufe die ganze Liste durch:
59
+ 24.786 Vergleiche fuer zehn Treffer, rund zwanzigmal je Neuzeichnen,
60
+ und bei laufenden Werten alle 700 ms (gemessen am Produktivsystem
61
+ 10.09.2026: Alias 51,9 ms, Quelle 15,8 ms je Zeichnen).
62
+
63
+ Die Binaersuche findet den Anfang in ~15 Schritten, danach wird nur
64
+ noch der Bereich selbst gelesen. */
65
+ export function mitVorspann(pre) {
66
+ var liste = S.keysSorted;
67
+ var lo = 0, hi = liste.length;
68
+ while (lo < hi) {
69
+ var m = (lo + hi) >> 1;
70
+ if (liste[m] < pre) { lo = m + 1; } else { hi = m; }
71
+ }
72
+ var raus = [];
73
+ for (var i = lo; i < liste.length && liste[i].indexOf(pre) === 0; i++) {
74
+ raus.push(liste[i]);
75
+ }
76
+ return raus;
77
+ }
78
+
55
79
  export function kindZustaende(kanal) {
56
80
  var pre = kanal + '.';
57
- return S.keysSorted.filter(function (k) {
58
- return k.indexOf(pre) === 0 && S.objects[k] && S.objects[k].type === 'state';
81
+ return mitVorspann(pre).filter(function (k) {
82
+ return S.objects[k] && S.objects[k].type === 'state';
59
83
  });
60
84
  }
61
85
 
@@ -64,6 +88,30 @@ export function direkteZustaende(kanal) {
64
88
  return kindZustaende(kanal).filter(function (k) { return k.slice(pre.length).indexOf('.') === -1; });
65
89
  }
66
90
 
91
+ /* Wohin schreibt dieser Aliaspunkt?
92
+
93
+ Fuenfmal woertlich gleich ausprogrammiert gewesen — dreimal in
94
+ `entwurf.js`, zweimal in `zuordnung.js` —, und die Kommentare an zwei
95
+ dieser Stellen dokumentieren je einen Fehler, der genau daraus
96
+ entstanden ist: einmal wurde aus einem schaltenden Punkt ein lesender
97
+ ohne Umrechnung, einmal verloren OPEN, SET und pct ihre Schreibquelle,
98
+ weil dort `q.einfach ? '' : …` stand und `common.write` gar nicht
99
+ angesehen wurde.
100
+
101
+ Die Regel selbst: Bei getrennten Quellen (`alias.id.read`/`.write`)
102
+ gilt, was unter `write` steht. Bei einer schlichten `alias.id` zeigen
103
+ Lesen und Schreiben auf denselben Punkt — geschrieben wird dorthin
104
+ aber nur, wenn der Alias das auch sagt: durch `common.write` oder
105
+ durch eine hinterlegte Schreibformel. Wer eine Schreibformel
106
+ hinterlegt hat, wollte schreiben. */
107
+ export function schreibQuelle(o) {
108
+ var c = (o && o.common) || {};
109
+ var a = c.alias || {};
110
+ var q = aliasQuellen(o);
111
+ if (!q.einfach) { return q.write || ''; }
112
+ return (c.write === true || typeof a.write === 'string') ? (q.write || '') : '';
113
+ }
114
+
67
115
  export function aliasQuellen(obj) {
68
116
  var a = obj && obj.common && obj.common.alias;
69
117
  if (!a) { return {}; }
@@ -114,17 +162,76 @@ export function holeEinzelne(ids, fertig) {
114
162
  }
115
163
 
116
164
  /* ================== Formel und Wert ================== */
165
+
166
+ /* Leseformeln sind fremder Code, der hier laeuft.
167
+
168
+ Sie stehen in `common.alias.read` jedes Alias und in `leseformel`
169
+ jeder Vorlage, und sie werden je Zeile bei JEDEM Zeichnen ausgefuehrt
170
+ — ungefragt, nicht auf Knopfdruck. Das ist keine neue
171
+ Rechteausweitung: Wer `common.alias.read` schreiben darf, hat
172
+ dieselbe Formel schon im js-controller laufen. Es ist aber ein
173
+ zusaetzlicher Ausfuehrungsort, und einer ohne Grenzen: `try/catch`
174
+ faengt nur Ausnahmen, kein `while(true){}`.
175
+
176
+ Zwei Schranken, beide billig:
177
+
178
+ 1. Gemerkt wird je (Formel, Rohwert). Dieselbe Formel auf demselben
179
+ Wert laeuft genau einmal — und genau das ist der Regelfall, wenn
180
+ alle 700 ms neu gezeichnet wird, ohne dass sich etwas geaendert
181
+ hat.
182
+ 2. Wer einmal zu lange gebraucht hat, laeuft nicht wieder. Die Zeile
183
+ sagt dann, warum. Den Reiter kann man damit noch einmal
184
+ einfrieren, aber nicht dauerhaft — und nach dem Neuladen steht
185
+ die Sperre wieder. */
186
+ var GEDULD_MS = 250;
187
+ var formelCache = new Map();
188
+ var formelGesperrt = {};
189
+
190
+ function cacheSchluessel(formel, roh) {
191
+ var t = typeof roh;
192
+ if (t === 'string') { return roh.length > 300 ? null : ('s:' + formel + '\u0000' + roh); }
193
+ if (t === 'number' || t === 'boolean') { return t.charAt(0) + ':' + formel + '\u0000' + roh; }
194
+ return null;
195
+ }
196
+
117
197
  export function auswerten(formel, roh) {
118
198
  if (roh === undefined || roh === null) { return { ok: false, txt: tr('detail.noValue') }; }
119
199
  if (!formel) { return { ok: true, val: roh }; }
200
+ if (formelGesperrt[formel]) {
201
+ return { ok: false, txt: tr('detail.formulaTooSlow', formelGesperrt[formel]), fehler: true };
202
+ }
203
+ var sch = cacheSchluessel(formel, roh);
204
+ if (sch !== null) {
205
+ var da = formelCache.get(sch);
206
+ if (da !== undefined) { return da; }
207
+ }
208
+ var raus;
209
+ var t0 = performance.now();
120
210
  try {
121
211
  var v = (new Function('val', 'return (' + formel + ');'))(roh);
122
- if (v === undefined) { return { ok: false, txt: 'undefined', leer: true }; }
123
- if (typeof v === 'number' && isNaN(v)) { return { ok: false, txt: 'NaN', leer: true }; }
124
- return { ok: true, val: v };
212
+ if (v === undefined) { raus = { ok: false, txt: 'undefined', leer: true }; }
213
+ else if (typeof v === 'number' && isNaN(v)) { raus = { ok: false, txt: 'NaN', leer: true }; }
214
+ else { raus = { ok: true, val: v }; }
125
215
  } catch (e) {
126
- return { ok: false, txt: tr('detail.formulaThrows', e.message), fehler: true };
216
+ raus = { ok: false, txt: tr('detail.formulaThrows', e.message), fehler: true };
127
217
  }
218
+ var gedauert = performance.now() - t0;
219
+ if (gedauert > GEDULD_MS) {
220
+ formelGesperrt[formel] = Math.round(gedauert);
221
+ console.warn('[alias-workbench] Formel gesperrt nach ' + Math.round(gedauert) + ' ms: ' + formel);
222
+ return { ok: false, txt: tr('detail.formulaTooSlow', Math.round(gedauert)), fehler: true };
223
+ }
224
+ if (sch !== null) {
225
+ /* Nicht unbegrenzt wachsen lassen: bei laufenden Werten entsteht je
226
+ neuem Rohwert ein Eintrag. Zweitausend reichen fuer ein Zeichnen
227
+ um ein Vielfaches; darueber wird der aelteste verworfen. */
228
+ if (formelCache.size >= 2000) {
229
+ var ersteR = formelCache.keys().next();
230
+ if (!ersteR.done) { formelCache.delete(ersteR.value); }
231
+ }
232
+ formelCache.set(sch, raus);
233
+ }
234
+ return raus;
128
235
  }
129
236
 
130
237
  var JA = ['ON', 'TRUE', 'YES', 'JA', '1'];
@@ -4,7 +4,7 @@
4
4
  import { S } from './zustand.js';
5
5
  import { el } from './basis.js';
6
6
  import { tr, txt } from './sprache.js';
7
- import { enums } from './enums.js';
7
+ import { enums, enumsUnbekannt } from './enums.js';
8
8
  import {
9
9
  enumListe,
10
10
  enumsVon,
@@ -14,13 +14,13 @@ import {
14
14
  } from './aufzaehlungen.js';
15
15
  import { enumVorlagen, katalogFehlt, ikonCache, ikonErlaubt, ikonNachtrag,
16
16
  ikonTaugt, vorlageZu, holeIkon } from './katalog.js';
17
- import { kindZustaende, aliasQuellen, holeEinzelne } from './werte.js';
17
+ import { kindZustaende, aliasQuellen, holeEinzelne, schreibQuelle } from './werte.js';
18
18
  import { aliasFuer, zielId, knotenDa } from './entwurf.js';
19
19
  import { zeichneErgebnis, entwurfAngefasst, angebotFrisch } from './ergebnis.js';
20
20
 
21
21
 
22
22
  import { ausgangName } from './vorlagen.js';
23
- import { musterVon } from './erkennung.js';
23
+ import { musterVon, rolleTrifft } from './erkennung.js';
24
24
 
25
25
  /* Welche Aufzaehlungen aendern sich, wenn dieser Entwurf geschrieben wird?
26
26
 
@@ -35,6 +35,14 @@ import { musterVon } from './erkennung.js';
35
35
  export function enumAenderungen(e) {
36
36
  var raus = [];
37
37
  if (!e || !e.kanal) { return raus; }
38
+ /* Ohne verlaesslichen Vorrat wird keine Aufzaehlung angefasst.
39
+
40
+ Was hier entstuende, waere aus der Vorlage gebaut und truege genau
41
+ ein Mitglied - `setObject` ersetzt das vorhandene Objekt damit
42
+ vollstaendig. Lieber gar kein Raum als ein Raum, der die anderen
43
+ Mitglieder verschluckt (09.09.2026). Die Oberflaeche sagt es am
44
+ Feld und im Trockenlauf. */
45
+ if (enumsUnbekannt()) { return raus; }
38
46
  /* Im Aliasmodus gibt es kein gewaehltes Ziel — der Alias *ist* das Ziel.
39
47
  Eine frueher hier stehende Abfrage brach in genau diesem Fall ab, und
40
48
  eine im Aliasmodus geaenderte Zuordnung wurde stillschweigend nicht
@@ -343,6 +351,11 @@ export function enumZeile(host, e) {
343
351
  });
344
352
  el2.appendChild(z);
345
353
  });
354
+ if (enumsUnbekannt()) {
355
+ var ef = el('div', 'vz zurueck');
356
+ ef.appendChild(el('span', null, tr('enums.unknown')));
357
+ el2.appendChild(ef);
358
+ }
346
359
  if (katalogFehlt) {
347
360
  var kf = el('div', 'vz zurueck');
348
361
  kf.appendChild(el('span', null, tr('catalog.missing')));
@@ -539,14 +552,47 @@ export function uebernehmeBestand(e) {
539
552
  Quelle zeigte `thermostat` statt `airCondition`, die Schreibquelle
540
553
  war weg, und von neun Punkten blieben zwei angehakt.
541
554
 
542
- Nur ohne Vorlage. Hat eine gegriffen, ist ihr Vorschlag gemeint -
543
- sonst uebertoente der Bestand sie stillschweigend, und „aendert
544
- sich" haette nichts mehr zu melden. */
555
+ Auch mit Vorlage. Hier stand `if (e.vorlage) return;`, mit der
556
+ Begruendung, sonst uebertoene der Bestand die Vorlage stillschweigend
557
+ und „aendert sich" haette nichts mehr zu melden. Das war zweimal
558
+ falsch.
559
+
560
+ Erstens ist es nicht mehr stillschweigend: `vorlagenWert` haelt fest,
561
+ was die Vorlage wollte, die Zeile traegt dann „weicht von der Vorlage
562
+ ab", und der ←-Knopf holt jedes Feld einzeln zurueck. Genau so haelt
563
+ es die Zwillingsfunktion `bestandVorrang` seit jeher - sie uebernimmt
564
+ den Bestand auch bei gesetzter Vorlage.
565
+
566
+ Zweitens widersprachen sich damit die beiden Ansichten, und das ist
567
+ der Punkt, den U21 verlangt: derselbe Alias, derselbe Stand. Gemessen
568
+ am 08.09.2026 produktiv an `alias.0.Wohnkueche.Lavalampe`: sieben der
569
+ acht Punkte stimmten (sie liefen ueber `bestandVorrang`), ACTUAL
570
+ nicht. Der laeuft ueber den Umbenennungs-Zweig unten - die Vorlage
571
+ `tasmota-lampe` nennt den Punkt ON_ACTUAL, im Alias heisst er ACTUAL
572
+ - und blieb deshalb auf dem Vorlagenstand stehen: `sensor.light`
573
+ statt `state`, Beschriftung leer statt „Lavalampe ACTUAL". Ein
574
+ Aktualisieren aus der Quellenansicht haette beides ueberschrieben,
575
+ und die Marke nannte im Tooltip nur die Rolle, nicht den Namen.
576
+
577
+ `vorlagenWert` wird nur gesetzt, wo noch keiner steht: fuer die
578
+ Zeilen, deren Name schon passt, hat `bestandVorrang` ihn vorher
579
+ gefuellt - mit dem echten Vorlagenstand. Ihn hier zu ueberschreiben
580
+ hiesse, dem Vorlagenknopf den Wert zu nehmen, den er zurueckholen
581
+ soll. */
545
582
  function vomAliasUebernehmen(z, o) {
546
- if (e.vorlage) { return; }
547
583
  var c = o.common || {}, a = c.alias || {}, q2 = aliasQuellen(o);
548
- if (c.role) { z.role = c.role; }
549
- if (c.type) { z.typ = c.type; }
584
+ if (!z.vorlagenWert) {
585
+ z.vorlagenWert = {
586
+ on: z.on, role: z.role, typ: z.typ, unit: z.unit, caption: z.caption,
587
+ f: z.f, fw: z.fw, srcR: z.srcR, srcW: z.srcW, states: z.states
588
+ };
589
+ }
590
+ /* `!== undefined`, nicht bloss wahr: ein Punkt mit leerer Rolle im
591
+ Alias hat sie bewusst leer, und die Zwillingsfunktion
592
+ `bestandVorrang` fragt genauso. Solange die beiden dieselbe Frage
593
+ stellen, kann der Stand nicht wieder auseinanderlaufen. */
594
+ if (c.role !== undefined) { z.role = c.role; }
595
+ if (c.type !== undefined) { z.typ = c.type; }
550
596
  z.unit = c.unit || '';
551
597
  z.states = c.states || undefined;
552
598
  z.f = (typeof a.read === 'string') ? a.read : '';
@@ -576,14 +622,24 @@ export function uebernehmeBestand(e) {
576
622
  Belegt ist das Schreiben durch `common.write` oder durch eine
577
623
  hinterlegte Schreibformel; bei getrennten Quellen zaehlt, was in
578
624
  `alias.id.write` steht. */
579
- z.srcW = q2.einfach
580
- ? ((c.write === true || typeof a.write === 'string') ? (q2.write || '') : '')
581
- : (q2.write || '');
625
+ z.srcW = schreibQuelle(o);
582
626
  if (q2.write && !q2.einfach && q2.write !== q2.read) {
583
627
  /* Die Gegenzeile eines Paars ist keine eigene mehr - ihre Quelle
584
628
  steckt jetzt als Schreibquelle in dieser Zeile. */
585
629
  e.states.forEach(function (x) {
586
- if (x !== z && x.srcR === q2.write) { x.on = false; }
630
+ if (x === z || x.srcR !== q2.write) { return; }
631
+ /* Aber nur, was nicht selbst im Alias steht.
632
+
633
+ Sonst haengt es an der Reihenfolge von `Object.keys`, ob ein
634
+ bestehender Punkt ueberlebt: wird er vor der Paarzeile
635
+ verarbeitet, bleibt er aus, die Aenderungs-Karte meldet „wird
636
+ entfernt", und das Schreiben loescht ihn — ohne dass ihn
637
+ jemand abgewaehlt haette. Heute rettet ihn die Schleife
638
+ darunter (`if (!s.on) s.on = true`), weil in allen 16
639
+ mitgelieferten Vorlagen die Stellzeile vor der Melderzeile
640
+ steht; das ist Glueck, keine Zusicherung (09.09.2026). */
641
+ if (x.n && S.objects[e.ziel + '.' + x.n]) { return; }
642
+ x.on = false;
587
643
  });
588
644
  }
589
645
  e.bestandZog = true;
@@ -662,9 +718,7 @@ export function uebernehmeBestand(e) {
662
718
  Schreibquelle, OPEN, SET und pct verloren sie - ein
663
719
  Aktualisieren haette den Rollladen ueber den Alias unfahrbar
664
720
  gemacht (Ricardo, 08.09.2026). */
665
- srcW: q.einfach
666
- ? ((c.write === true || typeof a.write === 'string') ? (q.write || '') : '')
667
- : (q.write || ''),
721
+ srcW: schreibQuelle(o),
668
722
  f: (typeof a.read === 'string') ? a.read : '',
669
723
  fw: (typeof a.write === 'string') ? a.write : '',
670
724
  caption: (nm && nm !== n) ? nm : '',
@@ -727,7 +781,7 @@ export function unterschiede(e) {
727
781
  if (!mu || !rolle) { return false; }
728
782
  return mu.states.some(function (st) {
729
783
  if (!st.role) { return false; }
730
- try { return new RegExp(st.role.source || st.role).test(rolle); } catch { return false; }
784
+ return rolleTrifft(st.role, rolle);
731
785
  });
732
786
  }
733
787
  /* Was die Vorlage von sich aus vorgeschlagen hat. Ein Punkt, der
@@ -23,7 +23,10 @@ export var S = {
23
23
  baumModusVorher: 'quellen',
24
24
  aufgeklappt: {},
25
25
  alleKlapp: null,
26
- abo: null,
26
+ /* Die laufenden Wertabos, als Liste von Mustern. War einmal ein
27
+ einzelnes Muster - seit im Aliasmodus auch die Quellen abonniert
28
+ werden, sind es mehrere (08.09.2026). */
29
+ abo: [],
27
30
  trockenAlle: null,
28
31
  /* Vergleich im Trockenlauf: Felder geordnet (Vorgabe) oder in der
29
32
  Reihenfolge, in der sie wirklich im Speicher stehen. */
@@ -39,6 +42,9 @@ export var S = {
39
42
  loeschZiel: null,
40
43
  loeschAlleAusgaenge: false,
41
44
  verlegeZiel: null,
45
+ /* Einmal registrierter Schliesser fuer die Quellenauswahl am
46
+ Sprungknopf - siehe ergebnis.js. */
47
+ sprungZuhoerer: false,
42
48
  VORLAGEN: [],
43
49
  vorlagenUnvollstaendig: false,
44
50
  fokusOrdnerfeld: false,
@@ -7,11 +7,11 @@
7
7
  werden oben bei der Musterwahl gebaut und hier nur eingehaengt. */
8
8
 
9
9
  import { S } from './zustand.js';
10
- import { el } from './basis.js';
10
+ import { el, $ } from './basis.js';
11
11
  import { tr } from './sprache.js';
12
12
  import { erkenneEntwurf, musterVon, rolleVonPlatz } from './erkennung.js';
13
13
  import { kindZustaende, wertVon, fmt } from './werte.js';
14
- import { steuerKanaele, vorlagenAbweichung, bestandsAbweichung, waehle } from './entwurf.js';
14
+ import { steuerKanaele, vorlagenAbweichung, bestandsAbweichung, waehle, quelleHier, quellenVerteilung } from './entwurf.js';
15
15
  import { musterName } from './musternamen.js';
16
16
  import { detailZeile } from './detail.js';
17
17
  import { rateAnzahl, rateZurueck, rateZurueckEine, rateBehalten } from './vorschlagen.js';
@@ -35,16 +35,13 @@ export function berechnePlaetze(e, haupt) {
35
35
  Object.keys(platzVon).forEach(function (n) {
36
36
  platzAnzahl[platzVon[n]] = (platzAnzahl[platzVon[n]] || 0) + 1;
37
37
  });
38
- var infoFund = erkenneEntwurf(e, 'info');
38
+ /* Hier stand ein voller Detektorlauf gegen das info-Muster. Sein
39
+ Ergebnis wanderte ueber `imInfo` und `infoNamen` in einen Parameter
40
+ von `setzeKnoepfe`, den dort niemand liest — bei JEDEM Zeichnen,
41
+ also bei laufenden Werten alle 700 ms (gefunden 09.09.2026).
42
+ `imInfo` bleibt als leeres Objekt stehen, damit die Aufrufer
43
+ unveraendert bleiben. */
39
44
  var imInfo = {};
40
- if (infoFund.length && haupt && haupt.type !== 'info') {
41
- infoFund[0].states.forEach(function (x) {
42
- if (x.id) {
43
- var kurz = x.id.slice(S.current.length + 1);
44
- if (!platzVon[kurz]) { imInfo[kurz] = true; }
45
- }
46
- });
47
- }
48
45
 
49
46
  /* Mehrere Kanaele, die fuer sich ein Geraet ergeben? Einmal rechnen,
50
47
  am Entwurf merken — der Hinweis in der Karte und die Knoepfe unten
@@ -98,6 +95,8 @@ export function berechnePlaetze(e, haupt) {
98
95
  uebernommenen Zeilen hatten im thermostat-Muster keinen Platz und
99
96
  wurden abgewaehlt, uebrig blieben zwei. */
100
97
  var bestandDa = !!(e.ziel && kindZustaende(e.ziel).length > 0);
98
+
99
+
101
100
  if (!e.vorbelegt && !e.vorlage && S.current.indexOf('alias.') !== 0) {
102
101
  e.vorbelegt = true;
103
102
  if (bestandDa) {
@@ -133,6 +132,38 @@ export function berechnePlaetze(e, haupt) {
133
132
  }
134
133
 
135
134
  export function baueListe(host, e, pl, rateKnopf, musterBlock) {
135
+ /* Bezugspunkt der Marken: die Quelle, an der ich gerade STEHE.
136
+
137
+ Markiert wird, was nicht von hier kommt. Erst lag der Bezug beim
138
+ angeklickten Knoten - dann bekamen an einer Sparte (`…Licht.stat`)
139
+ alle Zeilen aus `…Licht.tele` eine Marke, obwohl es dasselbe Geraet
140
+ ist. Danach lag er bei der Hauptquelle - dann waren an
141
+ `…Stromzaehler.stat` genau die Zeilen markiert, die von DORT kamen,
142
+ also die falschen (Ricardo, 08.09.2026).
143
+
144
+ `quelleHier` loest beides: es sucht den Eintrag der Verteilung, zu
145
+ dem dieser Knoten gehoert - Sparten fallen darin schon zusammen. Am
146
+ Alias selbst steht man nirgends; dort gilt die Hauptquelle. Findet
147
+ sich gar kein Bezug (ein Sammelknoten weit ueber den Quellen), wird
148
+ nichts markiert - eine Marke ohne Standpunkt sagt nichts. */
149
+ var istAliasSicht = S.current.indexOf('alias.') === 0;
150
+ var aliasFuerMarke = istAliasSicht
151
+ ? S.current
152
+ : (e.ziel && kindZustaende(e.ziel).length ? e.ziel : '');
153
+ /* Am Alias steht man nirgends - dort tragen ALLE Zeilen ihren Tag,
154
+ sobald es mehrere Quellen gibt.
155
+
156
+ Vorher galt auch dort die Mehrheit, und nur die Minderheit war
157
+ markiert. Das ist willkuerlich: bei drei zu drei entscheidet, wessen
158
+ Punkt alphabetisch zuerst kommt, und ein Umbenennen kippt die
159
+ Markierung auf die andere Haelfte (Ricardo, 08.09.2026). Traegt
160
+ jede Zeile ihren Tag, gibt es keine Mehrheit mehr zu bestimmen. */
161
+ var vert = aliasFuerMarke ? quellenVerteilung(aliasFuerMarke) : [];
162
+ var alleTaggen = istAliasSicht && vert.length > 1;
163
+ var hauptQuelle = aliasFuerMarke
164
+ ? (istAliasSicht ? '' : (quelleHier(aliasFuerMarke, S.current) || ''))
165
+ : (e.kanal || '');
166
+
136
167
  var platzVon = pl.platzVon, platzAnzahl = pl.platzAnzahl;
137
168
 
138
169
  /* --- Gerätekarte ---
@@ -210,6 +241,7 @@ export function baueListe(host, e, pl, rateKnopf, musterBlock) {
210
241
  dazu haengt sich erst danach in die Leiste, weil vorher niemand
211
242
  weiss, ob es ueberhaupt eine braucht. */
212
243
  var ohnePlatzZahl = 0;
244
+ var ohnePlatzNamen = [];
213
245
  var an = e.states.filter(function (s) { return s.on; }).length;
214
246
  bar.appendChild(document.createTextNode(tr('list.willBeCreated')));
215
247
  bar.appendChild(el('span', 'cnt', tr('list.countOf', an, e.states.length)));
@@ -325,6 +357,16 @@ export function baueListe(host, e, pl, rateKnopf, musterBlock) {
325
357
  });
326
358
  card.appendChild(cap);
327
359
 
360
+ /* Platz fuer die Legende. Sie steht zwischen Kopfzeile und erster
361
+ Zeile — nicht mehr oben in der Leiste, wo sie wie eine Marke zum
362
+ Geraet aussah und niemand sie mit der Toenung der Zeilen darunter
363
+ verband (Ricardo, 08.09.2026). Hier ist sie selbst ein Beispiel:
364
+ dieselbe Toenung, dieselbe Breite, unmittelbar ueber dem, was sie
365
+ erklaert. Gefuellt wird sie erst nach dem Zeichnen der Zeilen —
366
+ vorher weiss niemand, ob es etwas zu erklaeren gibt. */
367
+ var legendenPlatz = el('div');
368
+ card.appendChild(legendenPlatz);
369
+
328
370
  /* --- Zustandsliste --- */
329
371
  e.states.forEach(function (s, i) {
330
372
  var pl = platzVon[s.n];
@@ -337,7 +379,7 @@ export function baueListe(host, e, pl, rateKnopf, musterBlock) {
337
379
  einmal als Legende ueber der Liste, und der lange Text haengt als
338
380
  Hinweis an der Zeile. */
339
381
  var ohnePlatz = (s.on && !unfertig && !pl);
340
- if (ohnePlatz) { ohnePlatzZahl++; }
382
+ if (ohnePlatz) { ohnePlatzZahl++; ohnePlatzNamen.push(s.n); }
341
383
  var row = el('div', 'erow' + (s.on ? '' : ' skip') + (S.openRow === i ? ' open' : '') +
342
384
  (ohnePlatz ? ' ohneplatz' : ''));
343
385
  row.tabIndex = 0;
@@ -367,10 +409,16 @@ export function baueListe(host, e, pl, rateKnopf, musterBlock) {
367
409
  row.appendChild(mk);
368
410
 
369
411
  var n3 = el('span', 'nm3', s.n || tr('list.newPoint'));
370
- /* Die Beschriftung (kuenftiger Anzeigename) steht gedaempft hinter
371
- der Kennung - wie die Namen im Baum. Vorher tippte man sie im
372
- Detail und sah nirgends eine Wirkung. */
373
- if (s.caption && s.caption !== s.n) { n3.appendChild(el('span', 'nam', s.caption)); }
412
+ /* Die Beschriftung (kuenftiger Anzeigename) steht im Hinweis an der
413
+ Kennung, nicht in der Zeile.
414
+
415
+ Sie stand einmal gedaempft dahinter - wie die Namen im Baum, und
416
+ aus demselben Grund: wer sie im Detail tippt, soll eine Wirkung
417
+ sehen. Seit die Zeile auch Marken traegt („aus mqtt-client.0",
418
+ „geaendert", „weicht ab") wurde sie damit zu voll, und Rolle und
419
+ Wert rutschten nach rechts weg (Ricardo, 08.09.2026). Die Kennung
420
+ traegt jetzt den Hinweis; sichtbar bleibt sie im Detail. */
421
+ if (s.caption && s.caption !== s.n) { n3.title = s.caption; }
374
422
  if (s.manuell) { n3.appendChild(el('span', 'hand', tr('list.byHand'))); }
375
423
  if (s.geaendert) { n3.appendChild(el('span', 'geae', tr('list.changed'))); }
376
424
  /* Was die Vorlage anders will, steht an der Zeile - sonst sieht man
@@ -409,7 +457,14 @@ export function baueListe(host, e, pl, rateKnopf, musterBlock) {
409
457
  Marken, jede mit eigener Bedeutung: „geaendert" heisst, der
410
458
  Nutzer war es; „weicht von der Vorlage ab" vergleicht mit der
411
459
  Vorlage; diese hier mit der Datenbank (Ricardo, 06.09.2026). */
412
- var bAbw = bestandsAbweichung(s, e.ziel);
460
+ /* `e.ziel || e.kanal` — wie bei den beiden Nachbarn.
461
+
462
+ Im Aliasmodus ist `e.ziel` nie gesetzt: Man steht ja schon auf dem
463
+ Alias, das Ziel waehlt man erst im Quellenmodus. Die Marke „weicht vom
464
+ Alias ab" erschien dort also NIE — ausgerechnet dort, wo sie am
465
+ meisten sagt (gemessen 09.09.2026: mit `e.ziel` null Abweichungen, mit
466
+ `e.ziel || e.kanal` eine). */
467
+ var bAbw = bestandsAbweichung(s, e.ziel || e.kanal);
413
468
  if (bAbw.length) {
414
469
  var bv = el('span', 'geae bstabw', tr('list.aliasDiffers'));
415
470
  bv.title = bAbw.map(function (x) {
@@ -421,6 +476,41 @@ export function baueListe(host, e, pl, rateKnopf, musterBlock) {
421
476
  /* Die Marke ist zugleich der Griff: solange sie steht, ist die Zeile
422
477
  unbestaetigt und faellt beim Zuruecknehmen. Ein Haken laesst sie
423
478
  verschwinden, ein Kreuz nimmt genau diese eine zurueck. */
479
+ /* Der Punkt liest woanders her als der Rest.
480
+
481
+ Bewusst eine Marke und keine Flaechentoenung: die Flaeche ist
482
+ schon vergeben („kein Platz im Muster"), und beides gilt
483
+ unabhaengig voneinander - eine Zeile kann getoent und fremd sein.
484
+ Die Marke traegt nur den Adapter, nicht die ganze Kennung; die
485
+ volle steht im Hinweis. Und sie ist ruhig gehalten: sie meldet
486
+ keinen Fehler, sie sagt, woher der Wert kommt. */
487
+ var vonHier = !!hauptQuelle && s.srcR &&
488
+ (s.srcR === hauptQuelle || s.srcR.indexOf(hauptQuelle + '.') === 0);
489
+ if (s.on && s.srcR && (alleTaggen || (hauptQuelle && !vonHier))) {
490
+ /* Der Tag nennt die QUELLE, nicht den Adapter.
491
+
492
+ Er trug erst nur `mqtt-client.0` - was reichte, solange nur die
493
+ Ausreisser markiert waren. Seit am Alias jede Zeile einen Tag
494
+ traegt, muss er unterscheiden: `alias.0.Solar.Balkon.Einspeisung`
495
+ liest aus zwei mqtt-Quellen, und beide hiessen „aus
496
+ mqtt-client.0" (08.09.2026).
497
+
498
+ Gezeigt werden die letzten zwei Glieder des Quellknotens -
499
+ `Solar_Balkon`, `HM600.ch0`, `Balkon.Einspeisung`. Kurz genug
500
+ fuer die Zeile, lang genug zum Unterscheiden; die volle Kennung
501
+ steht im Hinweis. */
502
+ var qHier = aliasFuerMarke ? (quelleHier(aliasFuerMarke, s.srcR) || '') : '';
503
+ var basis = qHier || s.srcR.slice(0, s.srcR.lastIndexOf('.'));
504
+ var teileQ = basis.split('.');
505
+ var kurzQ = teileQ.slice(Math.max(2, teileQ.length - 2)).join('.') || basis;
506
+ var fq = el('span', 'geae fremdquelle', tr('list.otherSource', kurzQ));
507
+ /* Am Alias ist es eine blosse Herkunftsangabe, an einer Quelle
508
+ ein Unterschied - der Hinweis sagt beides jeweils richtig. */
509
+ fq.title = alleTaggen
510
+ ? tr('list.sourceHint', s.srcR)
511
+ : tr('list.otherSourceHint', hauptQuelle, s.srcR);
512
+ n3.appendChild(fq);
513
+ }
424
514
  if (s.geraten) {
425
515
  n3.appendChild(el('span', 'rat', tr('guess.mark')));
426
516
  var jaK = el('span', 'ratknopf', '\u2713');
@@ -554,16 +644,57 @@ export function baueListe(host, e, pl, rateKnopf, musterBlock) {
554
644
  });
555
645
  card.appendChild(add);
556
646
 
557
- /* Die Legende zum Streifen — einmal, und nur wenn es etwas zu erklaeren
558
- gibt. Sie steht in der Leiste ueber der Liste, gleich neben der Zahl:
559
- dort sucht man die Erklaerung fuer „11 von 42", nicht am Fuss. */
647
+ /* Die Legende — einmal, und nur wenn es etwas zu erklaeren gibt.
648
+
649
+ Zwei Anlaeufe hat sie gebraucht. Zuerst stand sie in der Leiste mit
650
+ einem 14 px grossen Farbkaestchen daneben: die Toenung ist
651
+ absichtlich blass, das Kaestchen war winzig, der Text in --ink-3 —
652
+ zusammen sah man nichts. Dann als hinterlegte Marke in der Leiste:
653
+ sichtbar, aber sie las sich wie eine Aussage ueber das GERAET, und
654
+ dass die getoenten Zeilen darunter gemeint sind, sagte sie nicht
655
+ (Ricardo, 08.09.2026, beide Male).
656
+
657
+ Jetzt steht sie in der Liste selbst, direkt ueber der ersten Zeile,
658
+ in derselben Toenung und ueber dieselbe Breite. Sie erklaert die
659
+ Toenung nicht, sie zeigt sie.
660
+
661
+ Die lange Erklaerung steht hinter einem „i" daneben — sie ersetzt
662
+ die Karte, die frueher unten stand („Gehoert nicht zum <Muster>").
663
+ Zweimal dieselbe Auskunft, einmal oben und einmal weit unten, war
664
+ eine zu viel.
665
+
666
+ Als Tooltip taugte sie nicht: der Kasten wurde ueber zehn Zeilen
667
+ hoch und legte sich ueber die Liste, die man gerade lesen wollte
668
+ (Ricardo, 08.09.2026). Ein Klick auf das „i" oeffnet sie stattdessen
669
+ als Dialog — dort darf sie so lang sein, wie sie ist. */
560
670
  if (ohnePlatzZahl) {
561
- var lg = el('span', 'legende');
562
- lg.appendChild(el('span', 'lgmarke'));
563
- lg.appendChild(document.createTextNode(
564
- tr('pattern.noPlaceIn', musterName(e.want) || e.want || '?')));
565
- lg.title = tr('pattern.noPlaceLong', musterName(e.want) || e.want || '?');
566
- bar.insertBefore(lg, bar.querySelector('.sp2'));
671
+ var mn = musterName(e.want) || e.want || '?';
672
+ var lg = el('div', 'legendenzeile');
673
+ lg.appendChild(document.createTextNode(tr('pattern.noPlaceLegend', mn)));
674
+
675
+ var ib = el('button', 'infoknopf', 'i');
676
+ ib.type = 'button';
677
+ ib.title = tr('info.more');
678
+ ib.setAttribute('aria-label', tr('info.more'));
679
+ ib.addEventListener('click', function (ev) {
680
+ ev.stopPropagation();
681
+ var t = $('#info-titel');
682
+ /* „Gehoert nicht zum Steckdose" waere falsches Deutsch — mit
683
+ „-Muster" dahinter stimmt es fuer jeden Musternamen. */
684
+ if (t) { t.textContent = tr('info.dlgTitle', mn); }
685
+ var b = $('#info-body');
686
+ if (b) {
687
+ b.textContent = '';
688
+ var k = el('div', 'aside w');
689
+ k.appendChild(el('b', null, ohnePlatzNamen.join(', ')));
690
+ k.appendChild(document.createTextNode(' — ' + tr('info.explain', mn)));
691
+ b.appendChild(k);
692
+ }
693
+ var d = $('#dlg-info');
694
+ if (d && !d.open) { d.showModal(); }
695
+ });
696
+ lg.appendChild(ib);
697
+ legendenPlatz.appendChild(lg);
567
698
  }
568
699
 
569
700
  host.appendChild(card);
package/admin/tab.html CHANGED
@@ -11,10 +11,24 @@
11
11
  Vorgabepalette auf. Ohne Parameter (Reiter direkt geoeffnet)
12
12
  entscheidet die Systemeinstellung hell/dunkel. */
13
13
  (function () {
14
+ /* Der Admin hat mehr Themen als vier.
15
+
16
+ `io-package.json` laesst Admin ab 7.6.20 zu, und dort gibt es
17
+ auch `blue` (dunkel) und `colored` (hell). Beide fielen durch
18
+ diese Pruefung, das Attribut blieb ungesetzt — und dann
19
+ entscheidet die SYSTEMeinstellung: ein dunkler Admin auf einem
20
+ hell eingestellten Rechner bekam die helle Palette mitten in
21
+ seine dunkle Umgebung. Entschieden wird deshalb nach der
22
+ Helligkeit, nicht nach dem Namen; unbekannte Namen fallen
23
+ weiterhin auf die Systemeinstellung zurueck, das ist die
24
+ ehrlichste Vermutung. */
25
+ var HELLIGKEIT = {
26
+ modernLight: 'modernLight', light: 'light', colored: 'modernLight',
27
+ modernDark: 'modernDark', dark: 'dark', blue: 'modernDark'
28
+ };
14
29
  var setze = function (name) {
15
- if (/^(modernLight|modernDark|light|dark)$/.test(name)) {
16
- document.documentElement.setAttribute('data-adminthema', name);
17
- }
30
+ var w = HELLIGKEIT[name];
31
+ if (w) { document.documentElement.setAttribute('data-adminthema', w); }
18
32
  };
19
33
  var m = /[?&]react=([A-Za-z]+)(?:&|$)/.exec(location.search);
20
34
  if (m) { setze(m[1]); }
@@ -120,6 +134,14 @@
120
134
  </div>
121
135
  </dialog>
122
136
 
137
+ <dialog id="dlg-info" style="max-width:600px;">
138
+ <h3 id="info-titel">Geh&ouml;rt nicht zum Muster</h3>
139
+ <div class="dbody" id="info-body"></div>
140
+ <div class="dfoot">
141
+ <button class="btn primary" data-close>Schlie&szlig;en</button>
142
+ </div>
143
+ </dialog>
144
+
123
145
  <dialog id="dlg-guess" style="max-width:560px;">
124
146
  <h3 id="guess-titel">Freie Pl&auml;tze trotz Vorlage?</h3>
125
147
  <div class="dbody" id="guess-body"></div>
@@ -68,7 +68,7 @@
68
68
  },
69
69
  {
70
70
  "name": "ON_TIME",
71
- "rolle": "level.timer",
71
+ "rolle": "level.timer.off",
72
72
  "typ": "number",
73
73
  "einheit": "s",
74
74
  "lesen": "1.ON_TIME",
@@ -50,14 +50,14 @@
50
50
  },
51
51
  {
52
52
  "name": "ON_TIME",
53
- "rolle": "level.timer",
53
+ "rolle": "level.timer.off",
54
54
  "typ": "number",
55
55
  "einheit": "s",
56
56
  "lesen": "1.ON_TIME",
57
57
  "schreiben": "1.ON_TIME",
58
58
  "optional": true,
59
59
  "vorgabeAus": true,
60
- "hinweis": "Einschaltdauer in Sekunden. Vor dem Einschalten gesetzt, schaltet der Aktor danach von allein wieder aus. Das light-Muster hat dafuer keinen Platz — angehakt landet der Punkt im info-Geraet."
60
+ "hinweis": "Einschaltdauer in Sekunden. Der Platz ON_TIME im Muster nimmt ihn auf."
61
61
  },
62
62
  {
63
63
  "name": "INHIBIT",