iobroker.alias-workbench 0.9.9 → 0.9.11

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.
@@ -61,11 +61,25 @@ export function rolleTrifft(r, rolle) {
61
61
  Ableitung bekam so ein Punkt gar keine: er belegte seinen Platz nie,
62
62
  liess sich beliebig oft hinzufuegen, und die Rolle fehlte auch in der
63
63
  Auswahlliste — der Platz war auf keinem Weg zu befuellen. */
64
+ /* Der fehlende Anfangsanker zaehlt nicht gegen den Platz.
65
+
66
+ `vacuumCleaner.MAP_URL` schreibt `/vacuum\.map\.url$/` - ohne `^`.
67
+ Gemeint ist derselbe eine Rollenname wie bei den anderen; der Platz
68
+ blieb aber ohne Rolle, weil hier beide Anker verlangt wurden
69
+ (gemessen 12.09.2026 ueber alle 53 Muster: ausser MAP_URL traf es
70
+ nur `media.COVER`, und dessen Ausdruck `/^media\.cover(\..*)$/`
71
+ beschreibt wirklich keinen einen Namen - der Platz hat dafuer eine
72
+ `defaultRole`).
73
+
74
+ Das Endezeichen bleibt Pflicht: ohne `$` waere `/^level\.dimmer/`
75
+ auch von `level.dimmer.irgendwas` erfuellt, und dann ist es kein
76
+ Name mehr, sondern ein Praefix. */
64
77
  export function rolleAusAusdruck(r) {
65
78
  var q = ausdruckText(r);
66
79
  if (!q) { return ''; }
67
- if (q.charAt(0) !== '^' || q.slice(-1) !== '$') { return ''; }
68
- var t = q.slice(1, -1).split(String.fromCharCode(92)).join('');
80
+ if (q.slice(-1) !== '$') { return ''; }
81
+ if (q.charAt(0) === '^') { q = q.slice(1); }
82
+ var t = q.slice(0, -1).split(String.fromCharCode(92)).join('');
69
83
  return /^[A-Za-z0-9_]+([.][A-Za-z0-9_]+)*$/.test(t) ? t : '';
70
84
  }
71
85
 
@@ -260,6 +274,36 @@ export function typenFuerRolle(musterName, rolle) {
260
274
  return raus;
261
275
  }
262
276
 
277
+ /* Welche angehakten Zeilen treffen ihren Platz in der Rolle, aber nicht
278
+ im Datentyp?
279
+
280
+ Ein solcher Punkt faellt lautlos aus dem Muster - und wenn es ein
281
+ Pflichtplatz war, greift das Muster gar nicht mehr. Dann hat auf
282
+ einmal KEINE Zeile einen Platz, und die Oberflaeche beschuldigte alle:
283
+ am Bastelzimmer_Licht standen nach einem Typwechsel an SET alle acht
284
+ Zeilen getoent da, das Musterfeld sagte „✕ SET fehlt", und die
285
+ Wahrheit stand nur in der aufgeklappten SET-Zeile (Ricardo,
286
+ 12.09.2026).
287
+
288
+ Hier steht die Frage einmal, damit Musterfeld, Legende und Zeile
289
+ dieselbe Antwort geben. `pflicht` sagt, ob der betroffene Platz ein
290
+ Pflichtplatz ist - nur dann kippt das ganze Muster. */
291
+ export function typKonflikte(states, musterName) {
292
+ var mu = musterName && musterVon(musterName);
293
+ if (!mu) { return []; }
294
+ var raus = [];
295
+ (states || []).forEach(function (s) {
296
+ if (!s.on || !s.role || !s.typ) { return; }
297
+ var moegliche = typenFuerRolle(musterName, s.role);
298
+ if (!moegliche.length || moegliche.indexOf(s.typ) > -1) { return; }
299
+ var platz = platzFuerRolle(musterName, s.role);
300
+ if (!platz) { return; }
301
+ raus.push({ n: s.n, typ: s.typ, erwartet: moegliche, platz: platz.name,
302
+ pflicht: !!platz.required });
303
+ });
304
+ return raus;
305
+ }
306
+
263
307
  /* Die Datentypen, die ein Platz zulaesst - als Liste.
264
308
 
265
309
  Drei Faelle, gemessen am type-detector 6.0.1: 622 Plaetze nennen
package/admin/js/mqtt.js CHANGED
@@ -655,6 +655,27 @@ function mqttCustom(g, thema) {
655
655
  zurueckzulesen ist ein Wettlauf: die Sicht hinkt dem frisch
656
656
  geschriebenen Objekt einen Wimpernschlag hinterher, und der
657
657
  Trockenlauf sperrte weiter wegen eines Punktes, den es laengst gab. */
658
+ /* Der Typ eines Sendepunktes: `mixed`, sobald eine Schreibformel im
659
+ Spiel ist.
660
+
661
+ Sonst rechnet der js-controller den Formelwert beim Schreiben auf den
662
+ Typ des Ziels zurueck, und die Formel war umsonst: `POWER` traegt im
663
+ Befehlswissen `typ: boolean` UND `schreibformel: val ? "ON" : "OFF"`.
664
+ Aus dem "ON" wurde am boolean-Ziel wieder true, auf der Leitung stand
665
+ true statt ON. Geschaltet hat die Tasmota trotzdem — sie nimmt beides —,
666
+ aber die Formel stand nutzlos im Alias, und der von der Werkbank
667
+ angelegte Punkt sah anders aus als jeder von mqtt-client angelegte
668
+ (die sind alle `mixed`). Am Produktivsystem war es genau EINER von 196
669
+ (Ricardo, 15.09.2026, an alias.0.Bastelzimmer.Deckenlicht).
670
+
671
+ Keine Ausnahme fuer POWER, sondern eine Regel, die sich selbst
672
+ durchsetzt — sie gilt ebenso fuer `Fade` und `LedTable`. Wo keine
673
+ Formel steht, bleibt der genaue Typ: aus `number` zieht der Admin
674
+ Min, Max und Einheit. */
675
+ function punktTyp(w) {
676
+ return w.schreibformel ? 'mixed' : (w.typ || 'mixed');
677
+ }
678
+
658
679
  export function mqttEinzelnStill(kanal, name, fertig) {
659
680
  var bau = mqttPunktBauen(kanal, name, 'neu');
660
681
  if (!bau) { return fertig && fertig(); }
@@ -694,7 +715,7 @@ function mqttPunktBauen(kanal, name, was) {
694
715
  var common0 = {
695
716
  name: name,
696
717
  role: w0.rolle || 'state',
697
- type: w0.typ || 'mixed',
718
+ type: punktTyp(w0),
698
719
  read: true, write: true,
699
720
  desc: tr('mq.createdBy'),
700
721
  custom: mqttCustom(g, thema0)
@@ -728,7 +749,7 @@ export function mqttEinzeln(kanal, name, was, knopf) {
728
749
  var common = {
729
750
  name: name,
730
751
  role: w.rolle || 'state',
731
- type: w.typ || 'mixed',
752
+ type: punktTyp(w),
732
753
  read: true, write: true,
733
754
  desc: tr('mq.createdBy'),
734
755
  custom: mqttCustom(g, thema)
@@ -10,9 +10,9 @@
10
10
  import { S } from './zustand.js';
11
11
  import { D, el, $ } from './basis.js';
12
12
  import { tr, sprachtext } from './sprache.js';
13
- import { VORSCHAU, musterVon, erkenneEntwurf, platzFuerRolle, typenFuerRolle } from './erkennung.js';
13
+ import { VORSCHAU, musterVon, erkenneEntwurf, platzFuerRolle, typenFuerRolle, typKonflikte } from './erkennung.js';
14
14
  import { anMusterAnpassen } from './vorlagen.js';
15
- import { ratePlaetze, rateZurueck, rateAnzahl, rateMoeglich } from './vorschlagen.js';
15
+ import { ratePlaetze, rateAnzahl } from './vorschlagen.js';
16
16
  import { zeichneErgebnis, entwurfAngefasst, knopfFrisch } from './ergebnis.js';
17
17
  import { musterName, musterZeile } from './musternamen.js';
18
18
 
@@ -52,51 +52,93 @@ export function baueMusterwahl(e, haupt, pflichtFehlt) {
52
52
  Er wird hier gebaut, weil er zum Muster gehoert - haengt aber unten
53
53
  in der Leiste ueber der Zustandsliste. Neben dem Musterfeld stand er
54
54
  am Rand; die Liste, die er veraendert, faengt darunter an. */
55
+ /* Der Trockenlauf, der beides beantwortet: ob der Knopf etwas
56
+ anzubieten haette, und - wenn nicht - warum nicht. Frueher lief er
57
+ zweimal (einmal fuer `rateMoeglich`, einmal fuer den Klick); einmal
58
+ reicht, das Ergebnis traegt den Grund gleich mit. */
59
+ var probe = ratePlaetze(e, true);
60
+
61
+ /* Was frueher ein Sammelsatz war („entweder alle Plaetze besetzt, oder
62
+ kein Datenpunkt passt"), steht jetzt als der Fall da, der wirklich
63
+ vorliegt - mit den Namen der Plaetze, die frei bleiben. Ohne die
64
+ Namen war die Meldung nicht nachpruefbar.
65
+
66
+ Gezeigt wird sie, solange keine Vermutung offen ist: dann fuehrt die
67
+ blaue Leiste das Wort, und zwei Meldungen uebereinander widersprechen
68
+ sich nur. */
69
+ function plaetzeText(frei) {
70
+ var kurz = frei.slice(0, 6).join(', ');
71
+ return frei.length > 6 ? tr('guess.slotsMore', kurz, frei.length - 6) : kurz;
72
+ }
73
+ function meldungZu(p) {
74
+ if (p.anzahl > 0) { return null; }
75
+ var satz = p.grund === 'voll' ? tr('guess.noneFull')
76
+ : p.grund === 'vergeben' ? tr('guess.noneUsed', plaetzeText(p.frei))
77
+ : p.grund === 'keinTreffer' ? tr('guess.noneFit', plaetzeText(p.frei))
78
+ : null;
79
+ /* Der Zusatz zur Handentscheidung (U15): Steht ein Platz nur deshalb
80
+ leer, weil die Zeile dazu von Hand entschieden ist, muss genau das
81
+ dastehen - sonst sieht es aus wie ein Fehler des Ratens. */
82
+ if (satz && p.handWeg) { satz += ' ' + tr('guess.noneHand', p.handWeg); }
83
+ return satz;
84
+ }
85
+ /* Ungefragt gezeigt wird sie nur, wo sie etwas erklaert: dass kein
86
+ Knopf dasteht, obwohl Plaetze frei sind. Der Fall „alle Plaetze
87
+ besetzt" bleibt stumm - er sagt „alles gut, nichts zu tun", und das
88
+ stuende dann an fast jedem Geraet (dieselbe Ueberlegung wie C27).
89
+ Ueber den Klick ist er weiter erreichbar. */
90
+ e.rateMeldung = (rateAnzahl(e) || probe.grund === 'voll')
91
+ ? null : meldungZu(probe);
92
+
55
93
  /* Und nur, wenn er etwas anzubieten haette: ohne freie Plaetze oder
56
94
  freie Zeilen ist der Knopf ein leeres Versprechen — dann faellt er
57
- weg, statt beim Klick „nichts gefunden" zu melden. Zuruecknehmen
58
- bleibt immer moeglich, solange Vermutungen im Entwurf stehen. */
95
+ weg, statt beim Klick „nichts gefunden" zu melden. Das Zuruecknehmen
96
+ steht seit dem 15.09.2026 nicht mehr auch hier: derselbe Knopf hiess
97
+ je nach Lage „Freie Plaetze vorschlagen" oder „Vorschlaege
98
+ verwerfen", und daneben stand in der blauen Leiste ein zweiter mit
99
+ demselben Wort. Verworfen wird dort - bei dem, was es betrifft. */
100
+ /* Der Rueckfrage-Dialog, wenn eine Vorlage greift (U27). Er sitzt hier
101
+ als eigene Funktion, weil ihn zwei Knoepfe brauchen: der in der
102
+ Leiste und der kleine neben dem Hinweis, der Muster wechselt und
103
+ gleich vorschlaegt. */
104
+ function mitVorlagenfrage(dann) {
105
+ var vq = null;
106
+ S.VORLAGEN.forEach(function (v) { if (v.id === e.vorlage) { vq = v; } });
107
+ var body = $('#guess-body');
108
+ body.textContent = '';
109
+ body.appendChild(el('div', 'aside w',
110
+ tr('guess.tplAsk', vq ? sprachtext(vq.name) : e.vorlage)));
111
+ /* onclick statt addEventListener: der Dialog ist einer fuer alle
112
+ Geraete, der Griff muss beim Oeffnen den alten ersetzen. */
113
+ $('#btn-guess-go').onclick = function () {
114
+ $('#dlg-guess').close();
115
+ dann();
116
+ };
117
+ $('#dlg-guess').showModal();
118
+ }
119
+
59
120
  var rateKnopf = null;
60
- if (rateAnzahl(e) || rateMoeglich(e)) {
121
+ if (probe.anzahl > 0) {
61
122
  /* Mit passender Vorlage ist Raten nicht der normale Weg - die
62
123
  Zuordnung steht ja schon. Der Knopf bleibt, tritt aber leise auf
63
124
  und fragt vor dem Lauf nach (Ricardo, 25.08.2026). */
64
125
  var beilaeufig = !!e.vorlage && !rateAnzahl(e);
65
- rateKnopf = el('button', 'btn mini' + (rateAnzahl(e) ? ' wichtig' : '')
126
+ rateKnopf = el('button', 'btn mini'
66
127
  + (beilaeufig ? ' leise' : '')
67
128
  /* Ein leiser Knopf pocht nicht - das Pochen wuerde genau die
68
129
  Aufmerksamkeit holen, die er nicht verdient. */
69
- + (!beilaeufig && knopfFrisch('raten:' + S.current + ':' + (rateAnzahl(e) ? 'zurueck' : 'vor')) ? ' frisch' : ''),
70
- rateAnzahl(e) ? tr('guess.undo') : tr('guess.button'));
130
+ + (!beilaeufig && knopfFrisch('raten:' + S.current + ':vor') ? ' frisch' : ''),
131
+ tr('guess.button'));
71
132
  rateKnopf.title = beilaeufig ? tr('guess.tplHint') : tr('guess.hint');
72
133
  var rateLos = function () {
73
- e.rateMeldung = ratePlaetze(e) ? null : tr('guess.none');
134
+ e.rateMeldung = meldungZu(ratePlaetze(e));
74
135
  entwurfAngefasst();
75
136
  S.openRow = null;
76
137
  zeichneErgebnis();
77
138
  };
78
139
  rateKnopf.addEventListener('click', function () {
79
- if (rateAnzahl(e)) {
80
- rateZurueck(e); e.rateMeldung = null;
81
- entwurfAngefasst();
82
- S.openRow = null;
83
- zeichneErgebnis();
84
- return;
85
- }
86
140
  if (!beilaeufig) { rateLos(); return; }
87
- var vq = null;
88
- S.VORLAGEN.forEach(function (v) { if (v.id === e.vorlage) { vq = v; } });
89
- var body = $('#guess-body');
90
- body.textContent = '';
91
- body.appendChild(el('div', 'aside w',
92
- tr('guess.tplAsk', vq ? sprachtext(vq.name) : e.vorlage)));
93
- /* onclick statt addEventListener: der Dialog ist einer fuer alle
94
- Geraete, der Griff muss beim Oeffnen den alten ersetzen. */
95
- $('#btn-guess-go').onclick = function () {
96
- $('#dlg-guess').close();
97
- rateLos();
98
- };
99
- $('#dlg-guess').showModal();
141
+ mitVorlagenfrage(rateLos);
100
142
  });
101
143
  }
102
144
 
@@ -138,7 +180,17 @@ export function baueMusterwahl(e, haupt, pflichtFehlt) {
138
180
  if (!r.length) {
139
181
  var fehlend = musterVon(m).states.filter(function (x) { return x.required; })
140
182
  .map(function (x) { return x.name; });
141
- if (fehlend.length) {
183
+ /* Ein Pflichtplatz kann auch deshalb leer sein, weil die Zeile
184
+ dafuer im falschen Datentyp steht - die Rolle stimmt, der Typ
185
+ nicht, und der Punkt faellt aus dem Muster. „SET fehlt" ist dann
186
+ die falsche Auskunft: SET ist da. Der Typ-Grund geht vor, wo es
187
+ einen gibt (Ricardo, 12.09.2026, am Bastelzimmer_Licht). */
188
+ var konflikt = typKonflikte(pruefE.states, m)
189
+ .filter(function (x) { return fehlend.indexOf(x.platz) > -1; })[0];
190
+ if (konflikt) {
191
+ mark = tr('pattern.fitsNotType', konflikt.platz, konflikt.typ,
192
+ konflikt.erwartet.join(tr('pattern.typeOr')));
193
+ } else if (fehlend.length) {
142
194
  mark = '✕ ' + tr('pattern.missing', fehlend.join(', '));
143
195
  } else {
144
196
  /* Kein Pflichtplatz fehlt und trotzdem passt nichts - dann liegt
@@ -174,6 +226,58 @@ export function baueMusterwahl(e, haupt, pflichtFehlt) {
174
226
  }
175
227
  function markeFuer(m) { return bewerte(m).mark; }
176
228
 
229
+ /* ---- Was nach dem Raten passen wuerde ----------------------------
230
+
231
+ Ricardos Klimaanlage ist der Fall: `thermostat` greift und belegt 3
232
+ von 24 Plaetzen, `airCondition` faellt durch, weil sein einziger
233
+ Pflichtplatz MODE die Rolle nicht traegt. Nach dem Raten stuenden
234
+ dort 9 von 29 statt 8 von 24 - nur sieht man das nirgends, weil
235
+ „passt nicht" das letzte Wort behaelt. Wer nicht weiss, dass es den
236
+ Knopf gibt, waehlt `airCondition` nie aus.
237
+
238
+ Also wird einmal durchgerechnet, was jedes Muster nach dem Raten
239
+ ergaebe. Gerechnet wird auf einer Kopie - am Entwurf aendert das
240
+ nichts. Das Raten liest nur Namen, Werteliste, Einheit und Typ,
241
+ keine Messwerte; das Ergebnis haengt also allein an den Zeilen und
242
+ wird beim blossen Neuzeichnen nicht ungueltig. Gemerkt wird es
243
+ deshalb an `e` und faellt erst, wenn eine Zeile sich aendert
244
+ (`entwurfAngefasst`) oder ein anderes Muster gewaehlt ist.
245
+
246
+ Gemessen am `midea.AC` (43 Punkte): ein Trockenlauf 1,2 ms, alle 51
247
+ Muster 35 ms - einmal je Geraeteauswahl, gegen rund 200 ms fuers
248
+ Auswaehlen selbst. */
249
+ function probeMuster(m) {
250
+ if (!musterVon(m)) { return null; }
251
+ var kopie = JSON.parse(JSON.stringify({ kanal: e.kanal, states: e.states, want: m }));
252
+ /* Dieselbe Vorbereitung wie in `bewerte`: beim Vorschlag entscheidet
253
+ nicht das Muster, sondern die Rollen, die das Auswaehlen setzen
254
+ wuerde. */
255
+ if (e.vorschlag && !e.roh && m !== e.want) { anMusterAnpassen(kopie, m); }
256
+ ratePlaetze(kopie);
257
+ var r = erkenneEntwurf(kopie, m);
258
+ if (!r.length) { return { passt: false }; }
259
+ return { passt: true, gesamt: r[0].states.length,
260
+ belegt: r[0].states.filter(function (x) { return x.id; }).length };
261
+ }
262
+
263
+ function aussicht() {
264
+ if (e.rateAussicht && e.rateAussicht.want === e.want) { return e.rateAussicht; }
265
+ var proM = {}, neuPassend = [];
266
+ if (e.states && e.states.length) {
267
+ alleTypen.forEach(function (m) {
268
+ var p = probeMuster(m);
269
+ if (!p) { return; }
270
+ proM[m] = p;
271
+ /* „Neu" heisst: passt jetzt nicht, nach dem Raten schon. Das
272
+ gewaehlte Muster gehoert nicht dazu - es ist kein anderes. */
273
+ if (p.passt && m !== e.want && !bewerte(m).passt) { neuPassend.push(m); }
274
+ });
275
+ }
276
+ e.rateAussicht = { want: e.want, proM: proM, neu: neuPassend,
277
+ selbst: proM[e.want] || null };
278
+ return e.rateAussicht;
279
+ }
280
+
177
281
  /* Welche Muster ohne Suchtext angeboten werden. Das erkannte und das
178
282
  gerade gewaehlte gehoeren immer dazu - sonst fiel rgbSingle heraus,
179
283
  sobald man einmal auf light gewechselt hatte, und der Weg zurueck
@@ -252,6 +356,7 @@ export function baueMusterwahl(e, haupt, pflichtFehlt) {
252
356
  return a.rang - b.rang;
253
357
  });
254
358
  mEintraege = treffer;
359
+ var auL = aussicht();
255
360
 
256
361
  mlist.textContent = '';
257
362
  if (!treffer.length) {
@@ -263,8 +368,13 @@ export function baueMusterwahl(e, haupt, pflichtFehlt) {
263
368
  var z = el('div', 'vz' + (n === mMarkiert ? ' an' : '')
264
369
  + (t0.m === e.want ? ' gewaehlt' : ''));
265
370
  z.appendChild(el('span', null, musterZeile(t0.m)));
371
+ /* Hinter „✕ MODE fehlt" steht, ob das Muster nach dem Raten doch
372
+ greift - sonst liest man nur, dass es nicht passt, und haelt es
373
+ fuer erledigt. */
374
+ var nachRaten = (!bewerte(t0.m).passt && auL.proM[t0.m] && auL.proM[t0.m].passt)
375
+ ? ' ' + tr('guess.alsoRow') : '';
266
376
  z.appendChild(el('span', 'tiefer',
267
- (t0.via ? tr('pattern.viaRole', t0.via) + ' ' : '') + markeFuer(t0.m)));
377
+ (t0.via ? tr('pattern.viaRole', t0.via) + ' ' : '') + markeFuer(t0.m) + nachRaten));
268
378
  /* mousedown statt click: sonst greift der Fokusverlust zuerst und
269
379
  die Liste ist weg, bevor der Klick ankommt. */
270
380
  z.addEventListener('mousedown', function (ev) {
@@ -311,6 +421,73 @@ export function baueMusterwahl(e, haupt, pflichtFehlt) {
311
421
  };
312
422
  mBeschriften();
313
423
 
424
+ /* Der Hinweis neben dem Feld: genannt wird EIN Muster - das beste -
425
+ und nur, wenn es wirklich besser ist als das gewaehlte.
426
+
427
+ Der erste Anlauf zaehlte bloss, wie viele Muster nach dem Vorschlag
428
+ dazukaemen, und schrieb bei mehreren „12 weitere Muster" hin. Am
429
+ `midea.AC` sind das tatsaechlich zwoelf - aber elf davon sind
430
+ Beifang: `blind` 4/19, `illuminance` 3/7, `windowTilt` 3/7 an einer
431
+ Klimaanlage. Eine Zahl, die Beifang mitzaehlt, verdeckt genau die
432
+ eine Auskunft, um die es geht (Ricardo, 15.09.2026): `airCondition`
433
+ 9/29 gegen `thermostat` 8/24.
434
+
435
+ Gemessen wird an den belegten Plaetzen, nicht am Anteil. 9 von 29
436
+ ist besser als 8 von 24, weil neun Punkte des Geraets einen Platz
437
+ bekommen statt acht - der Anteil waere hier sogar knapp kleiner.
438
+ Beide Zahlen stehen NACH dem Vorschlag; „3/24" ist der Stand jetzt
439
+ und gehoert nicht daneben.
440
+
441
+ Ist nichts besser, steht hier nichts - der haeufigste Fall bleibt
442
+ damit stumm. */
443
+ var auF = aussicht();
444
+ var bestes = null;
445
+ auF.neu.forEach(function (m) {
446
+ if (!bestes || auF.proM[m].belegt > auF.proM[bestes].belegt) { bestes = m; }
447
+ });
448
+ /* Der Vergleichswert ist der Stand des gewaehlten Musters NACH dem
449
+ Vorschlag. Passt es auch dann nicht, ist jedes andere besser. */
450
+ var jetzt = (auF.selbst && auF.selbst.passt) ? auF.selbst.belegt : -1;
451
+ if (bestes && auF.proM[bestes].belegt > jetzt) {
452
+ var pB = auF.proM[bestes], nB = musterName(bestes) || bestes;
453
+ var hw = el('span', 'ratAussicht');
454
+ var htx = el('span', null, '· ' + tr('guess.better', nB));
455
+ htx.title = jetzt >= 0
456
+ ? tr('guess.betterHint', nB, pB.belegt + '/' + pB.gesamt,
457
+ musterName(e.want) || e.want, auF.selbst.belegt + '/' + auF.selbst.gesamt)
458
+ : tr('guess.betterHintNone', nB, pB.belegt + '/' + pB.gesamt,
459
+ musterName(e.want) || e.want);
460
+ hw.appendChild(htx);
461
+
462
+ /* Der Weg dorthin in einem Griff: wechseln UND vorschlagen.
463
+ Sonst waere der Hinweis eine Ansage ohne Handlung - man muesste
464
+ das Muster in der Liste suchen und danach noch den Knopf in der
465
+ Leiste finden (Ricardo, 16.09.2026).
466
+
467
+ Ein Zeichen statt eines Wortes: neben dem Satz bleiben rund 55 px,
468
+ „wechseln" braucht 58. Dieselbe Bauart wie die Uebernahmeknoepfe
469
+ im Detail (`←`, `⟲`) - kleines Zeichen, ausfuehrlicher Tooltip. */
470
+ var hgo = el('button', 'btn winzig rtgo', '→');
471
+ hgo.title = tr('guess.betterGoHint', nB, tr('guess.button'));
472
+ hgo.addEventListener('click', function (ev) {
473
+ ev.stopPropagation();
474
+ var los = function () {
475
+ /* Beim Vorschlag entscheidet nicht das Muster, sondern die
476
+ Rollen, die das Auswaehlen setzt - genau wie in `mWaehlen`. */
477
+ if (e.vorschlag && !e.roh) { anMusterAnpassen(e, bestes); } else { e.want = bestes; }
478
+ e.rateMeldung = meldungZu(ratePlaetze(e));
479
+ entwurfAngefasst();
480
+ S.openRow = null;
481
+ zeichneErgebnis();
482
+ };
483
+ /* Greift eine Vorlage, fragt auch dieser Weg nach (U27) - er raet
484
+ ja genauso ueber eine fertige Zuordnung hinweg. */
485
+ if (e.vorlage && !rateAnzahl(e)) { mitVorlagenfrage(los); } else { los(); }
486
+ });
487
+ hw.appendChild(hgo);
488
+ vw.appendChild(hw);
489
+ }
490
+
314
491
  sel.addEventListener('focus', function () {
315
492
  sel.value = '';
316
493
  mMarkiert = -1;
@@ -41,6 +41,7 @@ export function ladeObjekte(danach) {
41
41
  if (--offen === 0) {
42
42
  S.keysSorted = Object.keys(S.objects).sort();
43
43
  S.kleinIndex = {};
44
+ kaputteAliaseNeu();
44
45
  ladeEnums();
45
46
  /* Erst die Einstellungen, dann die Vorlagen - sonst holt die
46
47
  Werkbank sie auch dann, wenn sie abgeschaltet sind. */
@@ -206,9 +207,39 @@ export function uebernimmObjekt(id, o) {
206
207
  /* Der Baum liest aus S.keysSorted, nicht aus S.objects. Wer einen Punkt
207
208
  hinzufuegt oder entfernt, muss den Index neu bauen — sonst kennt der
208
209
  Baum ihn nicht, egal wie oft man zeichnet. */
210
+ /* Aliase, bei denen mindestens ein Punkt auf eine Quelle zeigt, die es
211
+ nicht (mehr) gibt.
212
+
213
+ Der teuerste Fehler am Alias: der js-controller merkt sich „Quelle
214
+ fehlt" dauerhaft und korrigiert das nie - nur loeschen und neu anlegen
215
+ hilft (siehe schreiben.js). Bis zum 14.09.2026 stand das nur im
216
+ aufgeklappten Detail und im Pruefungen-Kasten; die Zeile in der Liste
217
+ schwieg, weil sie „ist eine Quelle eingetragen" fragte statt „gibt es
218
+ sie".
219
+
220
+ Einmal gerechnet und gemerkt, nicht bei jedem Zeichnen: den Baum baut
221
+ die Werkbank an sechzehn Stellen neu. Gemessen am Produktivsystem:
222
+ 531 Alias-Punkte in 5,2 ms. */
223
+ export function kaputteAliaseNeu() {
224
+ var raus = {};
225
+ Object.keys(S.objects).forEach(function (id) {
226
+ if (id.indexOf('alias.') !== 0) { return; }
227
+ var ob = S.objects[id];
228
+ if (!ob || ob.type !== 'state') { return; }
229
+ var a = (ob.common || {}).alias;
230
+ if (!a || !a.id) { return; }
231
+ var ziele = (typeof a.id === 'string') ? [a.id] : [a.id.read, a.id.write];
232
+ if (!ziele.some(function (q) { return q && !S.objects[q]; })) { return; }
233
+ var kanal = id.split('.').slice(0, -1).join('.');
234
+ raus[kanal] = (raus[kanal] || 0) + 1;
235
+ });
236
+ S.aliasKaputt = raus;
237
+ }
238
+
209
239
  export function indexNeu() {
210
240
  S.keysSorted = Object.keys(S.objects).sort();
211
241
  S.kleinIndex = {};
242
+ kaputteAliaseNeu();
212
243
  zeigeObjektzahl();
213
244
  }
214
245
 
@@ -1,9 +1,9 @@
1
- /* Die sieben Pruefungen - laufen immer mit, fassen nichts an. */
1
+ /* Die acht Pruefungen - laufen immer mit, fassen nichts an. */
2
2
 
3
3
  import { S } from './zustand.js';
4
4
  import { $, el } from './basis.js';
5
5
  import { tr } from './sprache.js';
6
- import { wertVon } from './werte.js';
6
+ import { wertVon, zielKann } from './werte.js';
7
7
  import './erkennung.js';
8
8
  import { sofortRueckmeldung, mqttEinstellung } from './mqtt.js';
9
9
 
@@ -94,6 +94,31 @@ export function pruefungSenden(e) {
94
94
  }
95
95
 
96
96
 
97
+ /* Kann das Ziel, was der Punkt von ihm verlangt?
98
+
99
+ Drei Fragen an fremde Objekte, die die Werkbank vorher nie gestellt hat:
100
+ Nimmt das Schreibziel den Wert der Formel an? Laesst es sich ueberhaupt
101
+ beschreiben? Meldet die Lesequelle etwas? Was dabei schiefgeht, faellt
102
+ nirgends auf — geschrieben wird trotzdem, gelesen auch, es kommt nur
103
+ nichts dabei heraus (Ricardo, 15.09.2026).
104
+
105
+ Hinweis, nicht Fehler: Ob am Ende wirklich nichts passiert, entscheidet
106
+ der Adapter hinter dem Punkt, und den kennt die Werkbank nicht. */
107
+ export function pruefungZielKann(e) {
108
+ var treffer = [];
109
+ e.states.forEach(function (s) {
110
+ if (!s.on) { return; }
111
+ zielKann(s).forEach(function (x) { treffer.push({ n: s.n, art: x.art, typ: x.typ }); });
112
+ });
113
+ if (!treffer.length) { return { s: 'ok', t: tr('check.targetCan'), d: tr('check.targetCanFine') }; }
114
+ var text = treffer.map(function (x) {
115
+ if (x.art === 'formelBlind') { return tr('check.targetVoidFormula', x.n, x.typ); }
116
+ if (x.art === 'nichtSchreibbar') { return tr('check.targetNoWrite', x.n); }
117
+ return tr('check.targetNoRead', x.n);
118
+ }).join(', ');
119
+ return { s: 'warn', t: tr('check.targetCan'), d: text };
120
+ }
121
+
97
122
  export function baueChecks(e, haupt, pflichtFehlt) {
98
123
  var an = e.states.filter(function (s) { return s.on; });
99
124
  var ohneQuelle = an.filter(function (s) { return s.srcR && !S.objects[s.srcR]; });
@@ -113,6 +138,7 @@ export function baueChecks(e, haupt, pflichtFehlt) {
113
138
  : haupt.type) : tr('check.noPatternFits') },
114
139
  pruefungSenden(e),
115
140
  pruefungRueckmeldung(e),
141
+ pruefungZielKann(e),
116
142
  pruefungDopplung(e)
117
143
  ];
118
144