iobroker.alias-workbench 0.8.3 → 0.9.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.
@@ -4,6 +4,55 @@
4
4
  import { S } from './zustand.js';
5
5
  import { D } from './basis.js';
6
6
 
7
+ /* Der Ausdruck eines Platzes als blosser Text — ohne Schraegstriche.
8
+
9
+ Der Detector liefert seine Muster ueber `ChannelDetector.getPatterns()`
10
+ (so haengt `src/detector-bundle.js` sie an `window`), und diese Funktion
11
+ gibt `role` nicht als RegExp weiter, sondern als `role.toString()` —
12
+ also MIT den Schraegstrichen:
13
+
14
+ /^indicator(\.maintenance)?\.(lowbat|battery)$/
15
+
16
+ Das ist bei uns der Normalfall, nicht die Ausnahme: ein RegExp bekommen
17
+ wir nur, wenn jemand die Muster direkt aus `typePatterns` liest, etwa
18
+ ein Test. Wer bloss `.source` abfragt und sonst `String(r)` nimmt, haelt
19
+ die Schraegstriche fuer Teil des Ausdrucks und findet danach weder den
20
+ Anker `^` noch das Ende `$`.
21
+
22
+ Gefunden am 08.09.2026 am Produktivsystem, und der eine Fehler zog weit:
23
+ `ROLLEN` hatte 210 statt 232 Eintraege (E27) — `indicator.lowbat` fehlte,
24
+ die Rolle, die hm-rpc schreibt —, `BRIGHTNESS` bekam in sechs Mustern gar
25
+ keine Rolle (E6), `plaetzeFuerRollen` fand zu keiner einzigen Rolle einen
26
+ Platz, und die Abweichungskarte behauptete „haette ohnehin nicht
27
+ gezaehlt" ueber Punkte mit Platz. Die Node-Tests sahen nichts davon: sie
28
+ fuettern die Zerlegung mit echten RegExp-Literalen.
29
+
30
+ Deshalb geht seither jede Stelle, die einen Platzausdruck liest, hier
31
+ durch — es sind sieben. */
32
+ export function ausdruckText(r) {
33
+ if (!r) { return ''; }
34
+ if (r.source !== undefined) { return r.source; }
35
+ var q = String(r);
36
+ var m = /^\/(.*)\/[a-z]*$/.exec(q);
37
+ return m ? m[1] : q;
38
+ }
39
+
40
+ /* Der Ausdruck als RegExp — fuer alle, die damit pruefen wollen. Wirft
41
+ nicht: ein unbrauchbarer Ausdruck gibt `null`, und der Aufrufer
42
+ entscheidet, was das heisst. */
43
+ export function ausdruckRegExp(r) {
44
+ var q = ausdruckText(r);
45
+ if (!q) { return null; }
46
+ try { return new RegExp(q); } catch { return null; }
47
+ }
48
+
49
+ /* Passt eine Rolle auf den Ausdruck eines Platzes? */
50
+ export function rolleTrifft(r, rolle) {
51
+ if (!rolle) { return false; }
52
+ var re = ausdruckRegExp(r);
53
+ return re ? re.test(rolle) : false;
54
+ }
55
+
7
56
  /* Die Rollen kommen aus dem Paket, nicht aus einer eigenen Liste —
8
57
  sonst pflegt man eine zweite Wahrheit, die auseinanderlaeuft. */
9
58
  /* Manche Plaetze nennen keine Standardrolle, sondern nur einen
@@ -13,8 +62,8 @@ import { D } from './basis.js';
13
62
  liess sich beliebig oft hinzufuegen, und die Rolle fehlte auch in der
14
63
  Auswahlliste — der Platz war auf keinem Weg zu befuellen. */
15
64
  export function rolleAusAusdruck(r) {
16
- if (!r) { return ''; }
17
- var q = (r.source !== undefined) ? r.source : String(r);
65
+ var q = ausdruckText(r);
66
+ if (!q) { return ''; }
18
67
  if (q.charAt(0) !== '^' || q.slice(-1) !== '$') { return ''; }
19
68
  var t = q.slice(1, -1).split(String.fromCharCode(92)).join('');
20
69
  return /^[A-Za-z0-9_]+([.][A-Za-z0-9_]+)*$/.test(t) ? t : '';
@@ -41,8 +90,8 @@ export function rolleVonPlatz(st) { return st.defaultRole || rolleAusAusdruck(st
41
90
  dann bleibt es bei der Vorgaberolle. Lieber eine Schreibweise zu wenig
42
91
  als eine erfundene in der Liste. */
43
92
  export function schreibweisenAus(r) {
44
- if (!r) { return []; }
45
- var q = (r.source !== undefined) ? r.source : String(r);
93
+ var q = ausdruckText(r);
94
+ if (!q) { return []; }
46
95
  if (q.charAt(0) !== '^' || q.slice(-1) !== '$') { return []; }
47
96
  q = q.slice(1, -1);
48
97
 
@@ -160,7 +209,7 @@ export function plaetzeFuerRollen(musterName) {
160
209
  ROLLEN.forEach(function (r) {
161
210
  mu.states.forEach(function (st) {
162
211
  if (raus[r] || !st.role) { return; }
163
- try { if (new RegExp(st.role.source || st.role).test(r)) { raus[r] = st.name; } } catch { /* unbrauchbarer Ausdruck in der Vorlage: zaehlt nicht */ }
212
+ if (rolleTrifft(st.role, r)) { raus[r] = st.name; }
164
213
  });
165
214
  });
166
215
  return raus;
@@ -1,18 +1,33 @@
1
- /* Die uebersetzten Namen der Muster, angezapft beim Geraete-Adapter.
1
+ /* Die uebersetzten Namen der Muster, angezapft beim Admin.
2
2
 
3
- Der type-detector kennt nur rohe Typnamen (socket, thermostat). Der
4
- Geraete-Adapter fuehrt fuer genau diese Typen fertige Uebersetzungen
5
- in elf Sprachen - in seinem Bundle, ein Block je Sprache, und jeder
6
- Block gibt sich am Eintrag "type-Device type" selbst zu erkennen.
7
- Wir holen den Block der Oberflaechensprache und schlagen darin nach;
8
- eine eigene Liste wuerde bei jedem type-detector-Update altern
9
- (Ricardos Punkt 13, 25.08.2026 - Weg 2, wie der Raum/Funktion-Katalog).
3
+ Der type-detector kennt nur rohe Typnamen (socket, thermostat) und
4
+ bringt selbst KEINE Uebersetzung mit - er ist eine Bibliothek
5
+ (`@iobroker/type-detector`), kein Adapter. Uebersetzt werden seine
6
+ Typen nur in den Sprachdateien der Adapter, die ihn benutzen, und
7
+ jeder schleppt seine eigene Kopie mit. Ein Block je Sprache, jeder
8
+ gibt sich am Eintrag "type-Device type" selbst zu erkennen; wir holen
9
+ den der Oberflaechensprache und schlagen darin nach. Eine eigene
10
+ Liste wuerde bei jedem type-detector-Update altern (Ricardos Punkt 13,
11
+ 25.08.2026 - Weg 2, wie der Raum/Funktion-Katalog).
10
12
 
11
- Bricht die Kette - Adapter fehlt, Bundle umgebaut - bleibt die Karte
12
- leer und ueberall steht der rohe Name. Kein Fehler, nur weniger
13
- Komfort. Beide Bundle-Fassungen werden verstanden: die neue (4.x)
14
- schreibt Werte in Backticks, die alte (1.x, produktiv) als
15
- JSON-Strings. */
13
+ Gefragt wird ausschliesslich der ADMIN. Bis zum 08.09.2026 war es der
14
+ Geraete-Adapter; Ricardos Begruendung fuer den Wechsel: Der Admin ist
15
+ immer installiert, `devices` nicht, und er wird besser gepflegt. Was
16
+ er nicht uebersetzt, bleibt eben roh - gemessen am 08.09.2026:
17
+ Admin 8.0.11 kennt alle 53 Typen, Admin 7.8.23 nur 44.
18
+
19
+ Der Weg dorthin geht ueber zwei Verzeichnisse, weil keines allein
20
+ reicht: `index.html` nennt bei Admin 8 das richtige Buendel, bei
21
+ Admin 7 aber nur vier Bootstrap-Haeppchen, und die fuehren nicht
22
+ weiter (34 Dateien durchsucht, nichts gefunden). Dort steht die Karte
23
+ in einer Datei, die erst `mf-manifest.json` nennt - der Admin laedt
24
+ seine Oberflaeche ueber Module Federation. Also beide Listen
25
+ zusammen, kurze Dateien zuerst.
26
+
27
+ Bricht die Kette - Aufbau geaendert, Datei zu gross -, bleibt die
28
+ Karte leer und ueberall steht der rohe Name. Kein Fehler, nur weniger
29
+ Komfort. Beide Schreibweisen werden verstanden: Werte in Backticks
30
+ (Admin 8) und als JSON-Strings (Admin 7). */
16
31
 
17
32
  import { sprache } from './sprache.js';
18
33
  import { holeText } from './basis.js';
@@ -44,26 +59,76 @@ function ziehe(src) {
44
59
  return Object.keys(raus).length ? raus : null;
45
60
  }
46
61
 
62
+ /* Welche Dateien der Admin ueberhaupt ausliefert. Zwei Verzeichnisse,
63
+ zusammengelegt und ohne Doppel - siehe Kopf: keines allein reicht
64
+ fuer beide Admin-Fassungen. */
65
+ function adminDateien() {
66
+ var raus = [];
67
+ function dazu(f) {
68
+ /* Fuehrendes `./` weg, sonst zaehlt dieselbe Datei zweimal: das
69
+ Manifest schreibt `assets/lib-….js`, das index.html
70
+ `./assets/lib-….js`. Ohne dieses Abschneiden stuenden 30 Eintraege
71
+ in einer Liste, die nach 25 abgeschnitten wird - und die gesuchte
72
+ Datei faellt genauso heraus wie damals bei der Sortierung nach
73
+ Namenslaenge. */
74
+ f = f.replace(/^\.\//, '');
75
+ if (raus.indexOf(f) === -1) { raus.push(f); }
76
+ }
77
+ /* Gesucht werden PFADE, keine blossen Dateinamen.
78
+
79
+ Beide Verzeichnisse schreiben den Ordner dazu, wo einer gehoert:
80
+ `"sync":["assets/lib-DSjRYjfP.js"]` im Manifest, `assets/…js` im
81
+ index.html. Wer nur den Dateinamen aufsammelt und stur `assets/`
82
+ davorsetzt, erfindet zwei Pfade, die es nicht gibt: das Manifest
83
+ nennt unter `remoteEntry` und `ssrRemoteEntry` je einen Namen mit
84
+ ausdruecklich leerem `"path":""` - die zwei Dateien liegen unter `/`.
85
+ Das gab beim Laden zwei 404 in der Konsole (A1, gefunden 08.09.2026
86
+ produktiv). Geholfen haben sie ohnehin nie: hinter remoteEntry steckt
87
+ der Einstieg von Module Federation, nicht die Sprachkarte.
88
+
89
+ Der Schraegstrich im Muster ist zugleich die Bedingung - ein nackter
90
+ Name kommt so gar nicht erst in die Liste, und ein Admin, der seine
91
+ Buendel eines Tages woanders ablegt, wird trotzdem gefunden. */
92
+ var pfade = /[A-Za-z0-9_\-.]+\/[A-Za-z0-9_\-./]+\.js/g;
93
+ return holeText('/mf-manifest.json')['catch'](function () { return ''; })
94
+ .then(function (mf) {
95
+ (mf.match(pfade) || []).forEach(dazu);
96
+ return holeText('/index.html')['catch'](function () { return ''; });
97
+ })
98
+ .then(function (html) {
99
+ (html.match(pfade) || []).forEach(dazu);
100
+ /* Reihenfolge bleibt, wie die Verzeichnisse sie nennen. Erst
101
+ wurde nach Namenslaenge sortiert - uebernommen vom
102
+ Aufzaehlungskatalog, wo es das Riesenbuendel nach hinten
103
+ schiebt. Hier war es genau falsch: der Name der gesuchten Datei
104
+ ist bei Admin 8 mit 143 Zeichen der laengste ueberhaupt, sie
105
+ rutschte ans Ende und fiel aus der Auswahl. Ungeordnet steht
106
+ sie an fuenfter Stelle von 21, bei Admin 7 an vierter von 19 -
107
+ beide Verzeichnisse nennen das Wichtige zuerst (gemessen
108
+ 08.09.2026). Das Groessenlimit unten haelt die dicken Buendel
109
+ ohnehin heraus. */
110
+ return raus.slice(0, 25);
111
+ });
112
+ }
113
+
47
114
  export function ladeMusterNamen(nachher) {
48
115
  if (geholt) { return; }
49
116
  geholt = true;
50
- holeText('/adapter/devices/tab.html').then(function (html) {
51
- /* Die tab.html nennt mehrere Bausteine (Bootstrap-Haeppchen,
52
- Vendor, Hauptbundle). Die Karte steckt im Hauptbundle - das
53
- heisst in beiden bekannten Fassungen index-<hash>.js, also
54
- dieses zuerst, die uebrigen als Rueckfall der Reihe nach. */
55
- var dateien = (html.match(/assets\/[A-Za-z0-9_\-.]+\.js/g) || [])
56
- .filter(function (x, i, a) { return a.indexOf(x) === i; })
57
- .sort(function (a, b) {
58
- return (b.indexOf('assets/index-') === 0 ? 1 : 0)
59
- - (a.indexOf('assets/index-') === 0 ? 1 : 0);
60
- })
61
- .slice(0, 8);
117
+ adminDateien().then(function (dateien) {
62
118
  if (!dateien.length) { throw new Error('kein Bundle'); }
63
119
  return dateien.reduce(function (kette, f) {
64
120
  return kette.then(function (fertig) {
65
121
  if (fertig) { return true; }
66
- return holeText('/adapter/devices/' + f).then(function (src) {
122
+ return holeText('/' + f).then(function (src) {
123
+ /* Die Grenze liegt hoeher als beim Aufzaehlungskatalog (3 MB):
124
+ bei Admin 8 steckt die Karte ausgerechnet im grossen
125
+ GUI-Buendel, gemessen 8,4 MB (08.09.2026). Mit der alten
126
+ Grenze wurde genau diese Datei uebersprungen und produktiv
127
+ blieb jeder Beiname roh. Teuer ist es nicht: derselbe Admin
128
+ hat das Buendel gerade selbst geladen, der zweite Griff
129
+ kommt aus dem Zwischenspeicher des Browsers. Alles darueber
130
+ ist kein Buendel mehr, sondern ein Versehen. */
131
+ if (src.length > 12000000) { return false; }
67
132
  var karte = ziehe(src);
68
133
  if (karte) { musterNamen = karte; return true; }
69
134
  return false;
@@ -110,6 +110,9 @@ export function beschrifteHtml() {
110
110
  setz('#tpl-titel', tr('tpls.title', ''));
111
111
  setz('#btn-tpl-save', tr('tpls.saveNew'));
112
112
  setz('#mqtt-titel', tr('mq.dialogTitle'));
113
+ /* Der Titel des Info-Dialogs traegt den Musternamen und wird beim
114
+ Oeffnen gesetzt; hier steht nur der Rueckfall ohne Muster. */
115
+ setz('#info-titel', tr('info.dlgTitle', ''));
113
116
  setz('#guess-titel', tr('guess.tplTitle'));
114
117
  setz('#btn-guess-go', tr('guess.tplGo'));
115
118
  setz('#tree .empty', tr('tree.loading'));
@@ -12,7 +12,7 @@ import { tasmotaBefehle } from './mqtt.js';
12
12
  import { socket } from './verbindung.js';
13
13
 
14
14
  import { tr, sprachtext } from './sprache.js';
15
- import { musterVon } from './erkennung.js';
15
+ import { musterVon, rolleTrifft } from './erkennung.js';
16
16
 
17
17
  /* Welche Instanz — der Reiter wird mit ?<instanz>&noFooter geladen. */
18
18
  var INSTANZ = (function () {
@@ -29,6 +29,24 @@ export function zeigeObjektzahl() {
29
29
  if (z) { z.textContent = tr('app.objectsCount', S.keysSorted.length); }
30
30
  }
31
31
 
32
+ /* Wie viele Wertabos laufen gerade? Steht neben der Objektzahl.
33
+
34
+ Nicht aus Neugier: die Abos werden beim Wechsel des Knotens abgemeldet
35
+ und neu gesetzt, und ob dabei wirklich alle wieder verschwinden, sieht
36
+ man sonst nirgends. Bleibt die Zahl beim Durchklicken stabil, stimmt
37
+ es; waechst sie, sammelt sich etwas an (Ricardo, 08.09.2026). */
38
+ export function zeigeAbozahl() {
39
+ var z = $('#meta-abos');
40
+ if (!z) { return; }
41
+ var n = (S.abo || []).length;
42
+ z.textContent = ' \u00b7 ' + (n === 1 ? tr('app.subsCount1') : tr('app.subsCount', n));
43
+ /* Die Muster gehoeren dazu, sonst ist die Zahl ein Raetsel: „2 Abos"
44
+ bei zwanzig sichtbaren Werten liest sich falsch, bis man sieht, dass
45
+ ein Muster einen ganzen Zweig deckt (Ricardo, 08.09.2026). */
46
+ z.title = tr('app.subsHint') + String.fromCharCode(10) +
47
+ (S.abo || []).map(function (mu) { return ' ' + mu; }).join(String.fromCharCode(10));
48
+ }
49
+
32
50
  export function setzeMeta() {
33
51
  var m = $('#meta');
34
52
  if (!m) { return; }
@@ -41,6 +59,8 @@ export function setzeMeta() {
41
59
  steht sie neben ihresgleichen; ein eigener Span, damit das Abo sie
42
60
  nachfuehren kann, ohne die ganze Zeile neu zu bauen. */
43
61
  m.appendChild(el('span', null, tr('app.objectsCount', S.keysSorted.length))).id = 'meta-objekte';
62
+ m.appendChild(el('span', null, '')).id = 'meta-abos';
63
+ zeigeAbozahl();
44
64
  if (S.vorlagenUnvollstaendig) {
45
65
  var w = el('span', 'chip bad', tr('tpls.loadFailed'));
46
66
  w.style.marginLeft = '8px';
@@ -559,7 +579,7 @@ export function anMusterAnpassen(e, neuTyp) {
559
579
  var s = null;
560
580
  muster.states.forEach(function (st) {
561
581
  if (s || !st.role) { return; }
562
- try { if (new RegExp(st.role.source || st.role).test(rolle)) { s = st; } } catch { /* dito: eine kaputte Rolle macht die Vorlage nicht passend */ }
582
+ if (rolleTrifft(st.role, rolle)) { s = st; } /* eine kaputte Rolle macht die Vorlage nicht passend */
563
583
  });
564
584
  return s;
565
585
  }
@@ -9,7 +9,7 @@ import { D, $, el, kurz } from './basis.js';
9
9
  import { tr, sprachtext, sprache } from './sprache.js';
10
10
  import { katalogFehlt } from './katalog.js';
11
11
  import { funktionsAuswahl, kennungZuFunktion } from './aufzaehlungen.js';
12
- import { musterVon } from './erkennung.js';
12
+ import { musterVon, rolleTrifft } from './erkennung.js';
13
13
  import { musterZeile, musterName } from './musternamen.js';
14
14
  import { ladeVorlagen, aendereVorlagen, setzeMeta, pruefeVorlage, INSTANZ_ID } from './vorlagen.js';
15
15
  import { kindZustaende, hatPunkt, feldAusFormel } from './werte.js';
@@ -551,8 +551,7 @@ function rollenBlock(rolle, geraetetyp, setzen) {
551
551
  if (must) {
552
552
  var treffer = null;
553
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; }
554
+ if (!treffer && pp.role && rolleTrifft(pp.role, rolle)) { treffer = pp; }
556
555
  });
557
556
  var hin = el('div', 'sugg');
558
557
  if (treffer) {
@@ -20,7 +20,7 @@ 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
 
@@ -539,14 +539,47 @@ export function uebernehmeBestand(e) {
539
539
  Quelle zeigte `thermostat` statt `airCondition`, die Schreibquelle
540
540
  war weg, und von neun Punkten blieben zwei angehakt.
541
541
 
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. */
542
+ Auch mit Vorlage. Hier stand `if (e.vorlage) return;`, mit der
543
+ Begruendung, sonst uebertoene der Bestand die Vorlage stillschweigend
544
+ und „aendert sich" haette nichts mehr zu melden. Das war zweimal
545
+ falsch.
546
+
547
+ Erstens ist es nicht mehr stillschweigend: `vorlagenWert` haelt fest,
548
+ was die Vorlage wollte, die Zeile traegt dann „weicht von der Vorlage
549
+ ab", und der ←-Knopf holt jedes Feld einzeln zurueck. Genau so haelt
550
+ es die Zwillingsfunktion `bestandVorrang` seit jeher - sie uebernimmt
551
+ den Bestand auch bei gesetzter Vorlage.
552
+
553
+ Zweitens widersprachen sich damit die beiden Ansichten, und das ist
554
+ der Punkt, den U21 verlangt: derselbe Alias, derselbe Stand. Gemessen
555
+ am 08.09.2026 produktiv an `alias.0.Wohnkueche.Lavalampe`: sieben der
556
+ acht Punkte stimmten (sie liefen ueber `bestandVorrang`), ACTUAL
557
+ nicht. Der laeuft ueber den Umbenennungs-Zweig unten - die Vorlage
558
+ `tasmota-lampe` nennt den Punkt ON_ACTUAL, im Alias heisst er ACTUAL
559
+ - und blieb deshalb auf dem Vorlagenstand stehen: `sensor.light`
560
+ statt `state`, Beschriftung leer statt „Lavalampe ACTUAL". Ein
561
+ Aktualisieren aus der Quellenansicht haette beides ueberschrieben,
562
+ und die Marke nannte im Tooltip nur die Rolle, nicht den Namen.
563
+
564
+ `vorlagenWert` wird nur gesetzt, wo noch keiner steht: fuer die
565
+ Zeilen, deren Name schon passt, hat `bestandVorrang` ihn vorher
566
+ gefuellt - mit dem echten Vorlagenstand. Ihn hier zu ueberschreiben
567
+ hiesse, dem Vorlagenknopf den Wert zu nehmen, den er zurueckholen
568
+ soll. */
545
569
  function vomAliasUebernehmen(z, o) {
546
- if (e.vorlage) { return; }
547
570
  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; }
571
+ if (!z.vorlagenWert) {
572
+ z.vorlagenWert = {
573
+ on: z.on, role: z.role, typ: z.typ, unit: z.unit, caption: z.caption,
574
+ f: z.f, fw: z.fw, srcR: z.srcR, srcW: z.srcW, states: z.states
575
+ };
576
+ }
577
+ /* `!== undefined`, nicht bloss wahr: ein Punkt mit leerer Rolle im
578
+ Alias hat sie bewusst leer, und die Zwillingsfunktion
579
+ `bestandVorrang` fragt genauso. Solange die beiden dieselbe Frage
580
+ stellen, kann der Stand nicht wieder auseinanderlaufen. */
581
+ if (c.role !== undefined) { z.role = c.role; }
582
+ if (c.type !== undefined) { z.typ = c.type; }
550
583
  z.unit = c.unit || '';
551
584
  z.states = c.states || undefined;
552
585
  z.f = (typeof a.read === 'string') ? a.read : '';
@@ -727,7 +760,7 @@ export function unterschiede(e) {
727
760
  if (!mu || !rolle) { return false; }
728
761
  return mu.states.some(function (st) {
729
762
  if (!st.role) { return false; }
730
- try { return new RegExp(st.role.source || st.role).test(rolle); } catch { return false; }
763
+ return rolleTrifft(st.role, rolle);
731
764
  });
732
765
  }
733
766
  /* 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';
@@ -98,6 +98,8 @@ export function berechnePlaetze(e, haupt) {
98
98
  uebernommenen Zeilen hatten im thermostat-Muster keinen Platz und
99
99
  wurden abgewaehlt, uebrig blieben zwei. */
100
100
  var bestandDa = !!(e.ziel && kindZustaende(e.ziel).length > 0);
101
+
102
+
101
103
  if (!e.vorbelegt && !e.vorlage && S.current.indexOf('alias.') !== 0) {
102
104
  e.vorbelegt = true;
103
105
  if (bestandDa) {
@@ -133,6 +135,38 @@ export function berechnePlaetze(e, haupt) {
133
135
  }
134
136
 
135
137
  export function baueListe(host, e, pl, rateKnopf, musterBlock) {
138
+ /* Bezugspunkt der Marken: die Quelle, an der ich gerade STEHE.
139
+
140
+ Markiert wird, was nicht von hier kommt. Erst lag der Bezug beim
141
+ angeklickten Knoten - dann bekamen an einer Sparte (`…Licht.stat`)
142
+ alle Zeilen aus `…Licht.tele` eine Marke, obwohl es dasselbe Geraet
143
+ ist. Danach lag er bei der Hauptquelle - dann waren an
144
+ `…Stromzaehler.stat` genau die Zeilen markiert, die von DORT kamen,
145
+ also die falschen (Ricardo, 08.09.2026).
146
+
147
+ `quelleHier` loest beides: es sucht den Eintrag der Verteilung, zu
148
+ dem dieser Knoten gehoert - Sparten fallen darin schon zusammen. Am
149
+ Alias selbst steht man nirgends; dort gilt die Hauptquelle. Findet
150
+ sich gar kein Bezug (ein Sammelknoten weit ueber den Quellen), wird
151
+ nichts markiert - eine Marke ohne Standpunkt sagt nichts. */
152
+ var istAliasSicht = S.current.indexOf('alias.') === 0;
153
+ var aliasFuerMarke = istAliasSicht
154
+ ? S.current
155
+ : (e.ziel && kindZustaende(e.ziel).length ? e.ziel : '');
156
+ /* Am Alias steht man nirgends - dort tragen ALLE Zeilen ihren Tag,
157
+ sobald es mehrere Quellen gibt.
158
+
159
+ Vorher galt auch dort die Mehrheit, und nur die Minderheit war
160
+ markiert. Das ist willkuerlich: bei drei zu drei entscheidet, wessen
161
+ Punkt alphabetisch zuerst kommt, und ein Umbenennen kippt die
162
+ Markierung auf die andere Haelfte (Ricardo, 08.09.2026). Traegt
163
+ jede Zeile ihren Tag, gibt es keine Mehrheit mehr zu bestimmen. */
164
+ var vert = aliasFuerMarke ? quellenVerteilung(aliasFuerMarke) : [];
165
+ var alleTaggen = istAliasSicht && vert.length > 1;
166
+ var hauptQuelle = aliasFuerMarke
167
+ ? (istAliasSicht ? '' : (quelleHier(aliasFuerMarke, S.current) || ''))
168
+ : (e.kanal || '');
169
+
136
170
  var platzVon = pl.platzVon, platzAnzahl = pl.platzAnzahl;
137
171
 
138
172
  /* --- Gerätekarte ---
@@ -210,6 +244,7 @@ export function baueListe(host, e, pl, rateKnopf, musterBlock) {
210
244
  dazu haengt sich erst danach in die Leiste, weil vorher niemand
211
245
  weiss, ob es ueberhaupt eine braucht. */
212
246
  var ohnePlatzZahl = 0;
247
+ var ohnePlatzNamen = [];
213
248
  var an = e.states.filter(function (s) { return s.on; }).length;
214
249
  bar.appendChild(document.createTextNode(tr('list.willBeCreated')));
215
250
  bar.appendChild(el('span', 'cnt', tr('list.countOf', an, e.states.length)));
@@ -325,6 +360,16 @@ export function baueListe(host, e, pl, rateKnopf, musterBlock) {
325
360
  });
326
361
  card.appendChild(cap);
327
362
 
363
+ /* Platz fuer die Legende. Sie steht zwischen Kopfzeile und erster
364
+ Zeile — nicht mehr oben in der Leiste, wo sie wie eine Marke zum
365
+ Geraet aussah und niemand sie mit der Toenung der Zeilen darunter
366
+ verband (Ricardo, 08.09.2026). Hier ist sie selbst ein Beispiel:
367
+ dieselbe Toenung, dieselbe Breite, unmittelbar ueber dem, was sie
368
+ erklaert. Gefuellt wird sie erst nach dem Zeichnen der Zeilen —
369
+ vorher weiss niemand, ob es etwas zu erklaeren gibt. */
370
+ var legendenPlatz = el('div');
371
+ card.appendChild(legendenPlatz);
372
+
328
373
  /* --- Zustandsliste --- */
329
374
  e.states.forEach(function (s, i) {
330
375
  var pl = platzVon[s.n];
@@ -337,7 +382,7 @@ export function baueListe(host, e, pl, rateKnopf, musterBlock) {
337
382
  einmal als Legende ueber der Liste, und der lange Text haengt als
338
383
  Hinweis an der Zeile. */
339
384
  var ohnePlatz = (s.on && !unfertig && !pl);
340
- if (ohnePlatz) { ohnePlatzZahl++; }
385
+ if (ohnePlatz) { ohnePlatzZahl++; ohnePlatzNamen.push(s.n); }
341
386
  var row = el('div', 'erow' + (s.on ? '' : ' skip') + (S.openRow === i ? ' open' : '') +
342
387
  (ohnePlatz ? ' ohneplatz' : ''));
343
388
  row.tabIndex = 0;
@@ -367,10 +412,16 @@ export function baueListe(host, e, pl, rateKnopf, musterBlock) {
367
412
  row.appendChild(mk);
368
413
 
369
414
  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)); }
415
+ /* Die Beschriftung (kuenftiger Anzeigename) steht im Hinweis an der
416
+ Kennung, nicht in der Zeile.
417
+
418
+ Sie stand einmal gedaempft dahinter - wie die Namen im Baum, und
419
+ aus demselben Grund: wer sie im Detail tippt, soll eine Wirkung
420
+ sehen. Seit die Zeile auch Marken traegt („aus mqtt-client.0",
421
+ „geaendert", „weicht ab") wurde sie damit zu voll, und Rolle und
422
+ Wert rutschten nach rechts weg (Ricardo, 08.09.2026). Die Kennung
423
+ traegt jetzt den Hinweis; sichtbar bleibt sie im Detail. */
424
+ if (s.caption && s.caption !== s.n) { n3.title = s.caption; }
374
425
  if (s.manuell) { n3.appendChild(el('span', 'hand', tr('list.byHand'))); }
375
426
  if (s.geaendert) { n3.appendChild(el('span', 'geae', tr('list.changed'))); }
376
427
  /* Was die Vorlage anders will, steht an der Zeile - sonst sieht man
@@ -421,6 +472,41 @@ export function baueListe(host, e, pl, rateKnopf, musterBlock) {
421
472
  /* Die Marke ist zugleich der Griff: solange sie steht, ist die Zeile
422
473
  unbestaetigt und faellt beim Zuruecknehmen. Ein Haken laesst sie
423
474
  verschwinden, ein Kreuz nimmt genau diese eine zurueck. */
475
+ /* Der Punkt liest woanders her als der Rest.
476
+
477
+ Bewusst eine Marke und keine Flaechentoenung: die Flaeche ist
478
+ schon vergeben („kein Platz im Muster"), und beides gilt
479
+ unabhaengig voneinander - eine Zeile kann getoent und fremd sein.
480
+ Die Marke traegt nur den Adapter, nicht die ganze Kennung; die
481
+ volle steht im Hinweis. Und sie ist ruhig gehalten: sie meldet
482
+ keinen Fehler, sie sagt, woher der Wert kommt. */
483
+ var vonHier = !!hauptQuelle && s.srcR &&
484
+ (s.srcR === hauptQuelle || s.srcR.indexOf(hauptQuelle + '.') === 0);
485
+ if (s.on && s.srcR && (alleTaggen || (hauptQuelle && !vonHier))) {
486
+ /* Der Tag nennt die QUELLE, nicht den Adapter.
487
+
488
+ Er trug erst nur `mqtt-client.0` - was reichte, solange nur die
489
+ Ausreisser markiert waren. Seit am Alias jede Zeile einen Tag
490
+ traegt, muss er unterscheiden: `alias.0.Solar.Balkon.Einspeisung`
491
+ liest aus zwei mqtt-Quellen, und beide hiessen „aus
492
+ mqtt-client.0" (08.09.2026).
493
+
494
+ Gezeigt werden die letzten zwei Glieder des Quellknotens -
495
+ `Solar_Balkon`, `HM600.ch0`, `Balkon.Einspeisung`. Kurz genug
496
+ fuer die Zeile, lang genug zum Unterscheiden; die volle Kennung
497
+ steht im Hinweis. */
498
+ var qHier = aliasFuerMarke ? (quelleHier(aliasFuerMarke, s.srcR) || '') : '';
499
+ var basis = qHier || s.srcR.slice(0, s.srcR.lastIndexOf('.'));
500
+ var teileQ = basis.split('.');
501
+ var kurzQ = teileQ.slice(Math.max(2, teileQ.length - 2)).join('.') || basis;
502
+ var fq = el('span', 'geae fremdquelle', tr('list.otherSource', kurzQ));
503
+ /* Am Alias ist es eine blosse Herkunftsangabe, an einer Quelle
504
+ ein Unterschied - der Hinweis sagt beides jeweils richtig. */
505
+ fq.title = alleTaggen
506
+ ? tr('list.sourceHint', s.srcR)
507
+ : tr('list.otherSourceHint', hauptQuelle, s.srcR);
508
+ n3.appendChild(fq);
509
+ }
424
510
  if (s.geraten) {
425
511
  n3.appendChild(el('span', 'rat', tr('guess.mark')));
426
512
  var jaK = el('span', 'ratknopf', '\u2713');
@@ -554,16 +640,57 @@ export function baueListe(host, e, pl, rateKnopf, musterBlock) {
554
640
  });
555
641
  card.appendChild(add);
556
642
 
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. */
643
+ /* Die Legende — einmal, und nur wenn es etwas zu erklaeren gibt.
644
+
645
+ Zwei Anlaeufe hat sie gebraucht. Zuerst stand sie in der Leiste mit
646
+ einem 14 px grossen Farbkaestchen daneben: die Toenung ist
647
+ absichtlich blass, das Kaestchen war winzig, der Text in --ink-3 —
648
+ zusammen sah man nichts. Dann als hinterlegte Marke in der Leiste:
649
+ sichtbar, aber sie las sich wie eine Aussage ueber das GERAET, und
650
+ dass die getoenten Zeilen darunter gemeint sind, sagte sie nicht
651
+ (Ricardo, 08.09.2026, beide Male).
652
+
653
+ Jetzt steht sie in der Liste selbst, direkt ueber der ersten Zeile,
654
+ in derselben Toenung und ueber dieselbe Breite. Sie erklaert die
655
+ Toenung nicht, sie zeigt sie.
656
+
657
+ Die lange Erklaerung steht hinter einem „i" daneben — sie ersetzt
658
+ die Karte, die frueher unten stand („Gehoert nicht zum <Muster>").
659
+ Zweimal dieselbe Auskunft, einmal oben und einmal weit unten, war
660
+ eine zu viel.
661
+
662
+ Als Tooltip taugte sie nicht: der Kasten wurde ueber zehn Zeilen
663
+ hoch und legte sich ueber die Liste, die man gerade lesen wollte
664
+ (Ricardo, 08.09.2026). Ein Klick auf das „i" oeffnet sie stattdessen
665
+ als Dialog — dort darf sie so lang sein, wie sie ist. */
560
666
  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'));
667
+ var mn = musterName(e.want) || e.want || '?';
668
+ var lg = el('div', 'legendenzeile');
669
+ lg.appendChild(document.createTextNode(tr('pattern.noPlaceLegend', mn)));
670
+
671
+ var ib = el('button', 'infoknopf', 'i');
672
+ ib.type = 'button';
673
+ ib.title = tr('info.more');
674
+ ib.setAttribute('aria-label', tr('info.more'));
675
+ ib.addEventListener('click', function (ev) {
676
+ ev.stopPropagation();
677
+ var t = $('#info-titel');
678
+ /* „Gehoert nicht zum Steckdose" waere falsches Deutsch — mit
679
+ „-Muster" dahinter stimmt es fuer jeden Musternamen. */
680
+ if (t) { t.textContent = tr('info.dlgTitle', mn); }
681
+ var b = $('#info-body');
682
+ if (b) {
683
+ b.textContent = '';
684
+ var k = el('div', 'aside w');
685
+ k.appendChild(el('b', null, ohnePlatzNamen.join(', ')));
686
+ k.appendChild(document.createTextNode(' — ' + tr('info.explain', mn)));
687
+ b.appendChild(k);
688
+ }
689
+ var d = $('#dlg-info');
690
+ if (d && !d.open) { d.showModal(); }
691
+ });
692
+ lg.appendChild(ib);
693
+ legendenPlatz.appendChild(lg);
567
694
  }
568
695
 
569
696
  host.appendChild(card);
package/admin/tab.html CHANGED
@@ -120,6 +120,14 @@
120
120
  </div>
121
121
  </dialog>
122
122
 
123
+ <dialog id="dlg-info" style="max-width:600px;">
124
+ <h3 id="info-titel">Geh&ouml;rt nicht zum Muster</h3>
125
+ <div class="dbody" id="info-body"></div>
126
+ <div class="dfoot">
127
+ <button class="btn primary" data-close>Schlie&szlig;en</button>
128
+ </div>
129
+ </dialog>
130
+
123
131
  <dialog id="dlg-guess" style="max-width:560px;">
124
132
  <h3 id="guess-titel">Freie Pl&auml;tze trotz Vorlage?</h3>
125
133
  <div class="dbody" id="guess-body"></div>