iobroker.alias-workbench 0.9.11 → 0.10.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,11 +4,11 @@ import { S } from './zustand.js';
4
4
  import { socket } from './verbindung.js';
5
5
  import { el } from './basis.js';
6
6
  import { tr } from './sprache.js';
7
- import { wertVon, jsonFelder, feldAusFormel, feldFormel, feldWert, jsonVon, zielKann } from './werte.js';
8
- import { musterVon, erkenneEntwurf, platzFuerRolle, typenFuerRolle } from './erkennung.js';
7
+ import { wertVon, jsonFelder, feldAusFormel, feldFormel, feldWert, jsonVon, zielKann, knopfZeile } from './werte.js';
8
+ import { musterVon, erkenneEntwurf, platzFuerRolle, typenFuerRolle, rolleVonPlatz } from './erkennung.js';
9
9
  import { musterName } from './musternamen.js';
10
10
  import { rollenFeld } from './rollenwahl.js';
11
- import { opt , quellenAuswahl, vorlagenAbweichung, bestandsAbweichung } from './entwurf.js';
11
+ import { opt , quellenAuswahl, vorlagenAbweichung, bestandsAbweichung, aboErgaenzen } from './entwurf.js';
12
12
  import { zeichneErgebnis, entwurfAngefasst } from './ergebnis.js';
13
13
 
14
14
  import { rateBehalten } from './vorschlagen.js';
@@ -26,7 +26,13 @@ export function detailZeile(e, s, idx) {
26
26
 
27
27
  /* Wer die Zeile anfasst, hat entschieden - die Vermutung ist damit
28
28
  keine mehr und faellt beim Zuruecknehmen nicht mehr weg. */
29
- function neu() { s.geaendert = true; rateBehalten(s); entwurfAngefasst(); zeichneErgebnis(); }
29
+ /* Eine von Hand umgestellte Quelle wird mit abonniert - sonst stuende
30
+ ihr Wert nach dem ersten Abholen still (J9b). */
31
+ function neu() {
32
+ s.geaendert = true; rateBehalten(s); entwurfAngefasst();
33
+ aboErgaenzen([s.srcR, s.srcW]);
34
+ zeichneErgebnis();
35
+ }
30
36
 
31
37
  /* ---- Die Auswahl der Datenpunkte ---------------------------------
32
38
 
@@ -100,9 +106,7 @@ export function detailZeile(e, s, idx) {
100
106
  if (!o || !o.common) { return null; }
101
107
  var c = o.common;
102
108
 
103
- var maengel = zielKann(s).filter(function (x) {
104
- return seite === 'w' ? x.art !== 'meldetNichts' : x.art === 'meldetNichts';
105
- });
109
+ var maengel = seite === 'w' ? zielKann(s) : [];
106
110
 
107
111
  var text, hinweis;
108
112
  if (seite === 'w') {
@@ -114,9 +118,13 @@ export function detailZeile(e, s, idx) {
114
118
  : (bl ? tr('detail.targetTypeVoid', c.type) : tr('detail.targetTypeHint', c.type));
115
119
  if (nw) { text = c.type + ' \u00b7 ' + tr('detail.notWritable'); }
116
120
  } else {
117
- if (!maengel.length) { return null; }
118
- text = tr('detail.reportsNothing');
119
- hinweis = tr('detail.targetNoRead');
121
+ /* Nur noch der Knopf, und ruhig: kein Mangel, eine Auskunft -
122
+ was `read: false` am Alias bewirkt und warum die Lesequelle
123
+ trotzdem da ist. Die Marke „aendert sich nie" an jeder Quelle
124
+ mit `read: false` ist seit 19.09.2026 weg (siehe `zielKann`). */
125
+ if (!knopfZeile(s)) { return null; }
126
+ text = tr('detail.readIsButton');
127
+ hinweis = tr('detail.readIsButtonHint');
120
128
  }
121
129
 
122
130
  var m = el('span', 'chip ' + (maengel.length ? 'warn' : 'mut'), text);
@@ -243,6 +251,121 @@ export function detailZeile(e, s, idx) {
243
251
  zeigeBestand(r, felder);
244
252
  }
245
253
 
254
+ /* Welcher Platz ist schon vergeben, und von welcher Zeile? Das steht
255
+ als Vermerk am Gruppenkopf der Rollenliste. */
256
+ var belegtVon = {};
257
+ /* Eigener Platz direkt aus dem Befund — ueber `belegtVon` allein fand
258
+ ihn nur die LETZTE Zeile eines mehrfach vergebenen Platzes (ACTUAL
259
+ bei `info`), alle anderen standen auf „kein Platz“. */
260
+ var eigenerPlatz = null;
261
+ if (e.want) {
262
+ var fundR = erkenneEntwurf(e, e.want);
263
+ if (fundR.length) {
264
+ fundR[0].states.forEach(function (x) {
265
+ if (!x.id) { return; }
266
+ var nx = x.id.slice(e.kanal.length + 1);
267
+ belegtVon[x.name] = nx;
268
+ if (nx === s.n) { eigenerPlatz = x.name; }
269
+ });
270
+ }
271
+ }
272
+ /* --- Platz im Muster ---
273
+
274
+ Umgekehrt zur Rolle: erst den Platz waehlen, und die Werkbank traegt
275
+ ein, was das Muster dafuer vorgibt — Rolle, Typ, Einheit, Werteliste.
276
+ Wer eine Zeile auf SET setzen wollte, musste bisher wissen, dass SET
277
+ `switch` heisst (Ricardo, 19.09.2026). Datenpunkt und Name bleiben;
278
+ dieselbe Vorgabe nimmt der Klick auf einen freien Platz unten, nur
279
+ dass der eine neue Zeile anlegt. */
280
+ var platzBox = null;
281
+ var mPl = e.want && musterVon(e.want);
282
+ if (mPl) {
283
+ var meinPlatz = eigenerPlatz;
284
+ var pb = el('div');
285
+ var selP = el('select', 'tx');
286
+ selP.style.width = '240px';
287
+ var oKein = opt('', tr('detail.slotNone'));
288
+ if (meinPlatz) { oKein.disabled = true; }
289
+ selP.appendChild(oKein);
290
+ var gesehen = {};
291
+ mPl.states.forEach(function (st) {
292
+ if (gesehen[st.name]) { return; }
293
+ gesehen[st.name] = 1;
294
+ var fremd = belegtVon[st.name] && belegtVon[st.name] !== s.n && !st.multiple;
295
+ var txt = st.name + (st.required ? ' · ' + tr('detail.slotRequired') : '') +
296
+ (fremd ? ' · ' + tr('roles.slotTaken', belegtVon[st.name]) : '');
297
+ var o = opt(st.name, txt);
298
+ if (fremd) { o.disabled = true; }
299
+ selP.appendChild(o);
300
+ });
301
+ /* Gewaehlt, aber nicht bekommen (etwa weil die Schreibrichtung nicht
302
+ passt): die Wahl bleibt im Feld stehen, darunter steht warum. */
303
+ var gewuenscht = !meinPlatz && s.platzWahl && gesehen[s.platzWahl] ? s.platzWahl : '';
304
+ selP.value = meinPlatz || gewuenscht;
305
+ selP.addEventListener('click', function (ev) { ev.stopPropagation(); });
306
+ selP.addEventListener('change', function () {
307
+ var st = null;
308
+ mPl.states.forEach(function (x) { if (!st && x.name === selP.value) { st = x; } });
309
+ if (!st) { return; }
310
+ s.platzWahl = st.name;
311
+ s.role = rolleVonPlatz(st);
312
+ var ty = Array.isArray(st.type) ? st.type[0] : st.type;
313
+ if (ty) { s.typ = ty; }
314
+ if (st.defaultUnit) { s.unit = st.defaultUnit; }
315
+ /* Ohne Werteliste bleibt so ein Platz leer, egal wie gut Rolle und
316
+ Typ passen — wie beim Klick auf einen freien Platz. */
317
+ if (st.statesDefined && !s.states) { s.states = st.defaultStates || { 0: 'None' }; }
318
+ /* Ein Platz, den das Muster nur lesen laesst (ELECTRIC_POWER,
319
+ ACTUAL …), bekommt kein Schreibziel. mqtt-client legt jeden Punkt
320
+ beschreibbar an, und die Werkbank trug deshalb die Quelle auch
321
+ bei „schreibt auf" ein — am 3D-Drucker schrieb die Leistung auf
322
+ `tele.SENSOR` (Ricardo, 19.09.2026). Weggenommen wird nur, nie
323
+ geraten: ein fehlendes Schreibziel bei SET setzt das Feld nicht. */
324
+ if (st.write === false) { s.srcW = ''; s.wr = false; }
325
+ neu();
326
+ });
327
+ pb.appendChild(selP);
328
+ /* Neben dem Namen ist kein Platz fuer einen Satz darunter — er steht
329
+ als Tooltip am Feld. */
330
+ selP.title = tr('detail.slotHint');
331
+ /* Gewaehlt, aber nicht bekommen — warum?
332
+
333
+ Die Schreibrichtung ist es fast nie: traegt ein Punkt genau die
334
+ Vorgaberolle des Platzes, prueft der type-detector `read`/`write`
335
+ gar nicht (ChannelDetector: „When the default role is assigned …
336
+ we can be a bit more laxe"). Und das Feld setzt immer die
337
+ Vorgaberolle. Der erste Hinweis an dieser Stelle behauptete
338
+ deshalb etwas Falsches (Ricardo, 19.09.2026: an tele.SENSOR mit
339
+ Schreibziel blieb ELECTRIC_POWER vergeben, ohne Hinweis — zu
340
+ Recht). Der haeufige Grund ist ein anderer: das Muster greift als
341
+ Ganzes nicht, weil ein Pflichtplatz fehlt — dann vergibt der
342
+ Detektor gar keinen Platz, auch diesen nicht. */
343
+ /* Greift das Muster als Ganzes nicht, steht das in der Legende ueber
344
+ der Liste — hier waere es an der falschen Zeile. */
345
+ if (gewuenscht && Object.keys(belegtVon).length > 0) {
346
+ pb.appendChild(el('div', 'aside w', tr('detail.slotNotGiven', gewuenscht)));
347
+ }
348
+ platzBox = pb;
349
+ }
350
+
351
+ /* Der Platz steht rechts neben dem Namen, mit seiner Bezeichnung links
352
+ daneben wie eine Zeilenbeschriftung: man waehlt ihn am Anfang, und
353
+ weiter unten musste man ihn suchen (Ricardo, 19.09.2026). */
354
+ function mitPlatz(inhalt) {
355
+ if (!platzBox) { return inhalt; }
356
+ var w = el('div', 'fields');
357
+ w.style.alignItems = 'flex-start';
358
+ w.appendChild(inhalt);
359
+ var pf = el('div');
360
+ pf.style.display = 'flex';
361
+ pf.style.gap = '8px';
362
+ pf.style.alignItems = 'flex-start';
363
+ pf.appendChild(el('div', 'k', tr('detail.slot')));
364
+ pf.appendChild(platzBox);
365
+ w.appendChild(pf);
366
+ return w;
367
+ }
368
+
246
369
  /* --- Name, nur bei selbst angelegten --- */
247
370
  if (s.manuell) {
248
371
  var iN = el('input', 'tx w');
@@ -255,24 +378,76 @@ export function detailZeile(e, s, idx) {
255
378
  s.n = iN.value.trim().toUpperCase().replace(/\s+/g, '_');
256
379
  neu();
257
380
  });
258
- zeile(tr('detail.name'), iN);
381
+ zeile(tr('detail.name'), mitPlatz(iN));
259
382
  } else if (s.urId) {
260
- zeile(tr('detail.objectId'), s.urId);
383
+ zeile(tr('detail.objectId'), mitPlatz(el('div', null, s.urId)));
261
384
  } else {
262
- zeile(tr('detail.willBecome'), (e.ziel || e.kanal) + '.' + s.n);
385
+ /* Der letzte Teil ist ein Feld: der Name bestimmt nur die Kennung,
386
+ nicht was der Punkt tut — das macht die Rolle. Wer `cmnd_POWER`
387
+ lieber `POWER` oder `SET` nennen will, konnte das bisher nirgends
388
+ (Ricardo, 19.09.2026). Nur hier, beim Neuanlegen: an einem
389
+ bestehenden Punkt (`urId`, oben) haengen Skripte und vis am Namen.
390
+
391
+ Bereinigt wie die Zielkennung (zielleiste.js), aber ohne Punkt —
392
+ ein Punkt machte aus dem Namen einen Unterordner. Gross- und
393
+ Kleinschreibung bleiben, wie sie getippt wurden. */
394
+ /* Aufgebaut wie „liest aus": oben das Feld, darunter klein die
395
+ volle Kennung, die daraus entsteht (Ricardo, 19.09.2026). */
396
+ var vorspann = (e.ziel || e.kanal) + '.';
397
+ var iNa = el('input', 'tx');
398
+ iNa.type = 'text';
399
+ iNa.value = s.n;
400
+ iNa.style.width = '260px';
401
+ iNa.style.maxWidth = '100%';
402
+ iNa.style.fontFamily = 'var(--mono)';
403
+ var nameFelder = el('div', 'fields');
404
+ nameFelder.appendChild(iNa);
405
+ var nameStapel = mitVollId(nameFelder, vorspann + s.n);
406
+ var nameVoll = nameStapel.querySelector('.vollid');
407
+ var bereinigt = function (v) {
408
+ return v.trim().replace(/\s+/g, '_').replace(/[^\w\-äöüÄÖÜß]/g, '_');
409
+ };
410
+ iNa.addEventListener('click', function (ev) { ev.stopPropagation(); });
411
+ /* Beim Tippen zieht die Kennung darunter mit — man sieht, was
412
+ entsteht, bevor man das Feld verlaesst. */
413
+ iNa.addEventListener('input', function () {
414
+ var v = bereinigt(iNa.value) || s.n;
415
+ nameVoll.textContent = vorspann + v;
416
+ nameVoll.title = vorspann + v;
417
+ });
418
+ iNa.addEventListener('change', function () {
419
+ var neuN = bereinigt(iNa.value);
420
+ if (!neuN) { iNa.value = s.n; return; }
421
+ if (neuN !== s.n && e.states.some(function (x) { return x !== s && x.n === neuN; })) {
422
+ iNa.value = s.n;
423
+ iNa.title = tr('detail.nameTaken', neuN);
424
+ iNa.classList.add('miss');
425
+ nameVoll.textContent = vorspann + s.n;
426
+ return;
427
+ }
428
+ s.n = neuN;
429
+ neu();
430
+ });
431
+ zeile(tr('detail.name'), mitPlatz(nameStapel));
263
432
  }
264
433
 
265
434
  /* --- Quelle lesen + JSON-Feld --- */
266
435
  var qf = el('div', 'fields');
267
436
 
268
437
  var selR = el('select', 'tx');
269
- selR.style.width = '260px';
438
+ /* 420 statt 260 px: gekuerzt wird hinten, und dort steht, was den
439
+ Punkt unterscheidet — `…3D_Drucker.` ohne `cmnd.POWER` sagte nichts
440
+ (Ricardo, 19.09.2026). `maxWidth` haelt es in schmalen Fenstern. */
441
+ selR.style.width = '420px';
442
+ selR.style.maxWidth = '100%';
270
443
  selR.appendChild(opt('', quellen.length ? tr('detail.pickOne') : tr('detail.noSource')));
271
444
  quellenIn(selR, quellen);
272
445
  if (s.srcR && quellen.indexOf(s.srcR) === -1) { selR.appendChild(opt(s.srcR, s.srcR + ' ' + tr('detail.elsewhere'))); }
273
446
  selR.appendChild(opt('__frei__', tr('detail.otherObject')));
274
447
  selR.value = s.srcR || '';
275
- if (!s.srcR) { selR.className += ' miss'; }
448
+ /* Ein Knopf darf ohne Lesequelle sein - dann ist das leere Feld kein
449
+ Fehler und wird nicht rot (Ricardo, 19.09.2026). */
450
+ if (!s.srcR && !knopfZeile(s)) { selR.className += ' miss'; }
276
451
  selR.addEventListener('click', function (ev) { ev.stopPropagation(); });
277
452
  selR.addEventListener('change', function () {
278
453
  if (selR.value === '__frei__') { s.frei = true; s.srcR = ''; }
@@ -293,7 +468,6 @@ export function detailZeile(e, s, idx) {
293
468
  neu();
294
469
  });
295
470
  var lR = el('label', 'fld');
296
- lR.appendChild(el('span', null, tr('detail.datapoint')));
297
471
  lR.appendChild(selR);
298
472
  qf.appendChild(lR);
299
473
 
@@ -338,10 +512,31 @@ export function detailZeile(e, s, idx) {
338
512
  zeile('Objekt-ID', iFrei);
339
513
  }
340
514
 
515
+ /* --- Leseformel ---
516
+
517
+ Direkt unter „liest aus“, wie die Schreibformel unter „schreibt auf“:
518
+ jede Formel steht bei der Quelle, fuer die sie rechnet (Ricardo,
519
+ 19.09.2026). */
520
+ var fb = el('div');
521
+ var iF = el('input', 'tx w');
522
+ iF.type = 'text';
523
+ iF.value = s.f || '';
524
+ iF.placeholder = tr('detail.formulaPlaceholder');
525
+ iF.addEventListener('click', function (ev) { ev.stopPropagation(); });
526
+ iF.addEventListener('change', function () { s.f = iF.value.trim(); neu(); });
527
+ fb.appendChild(iF);
528
+ /* Der Hinweis gilt nur fuer Schaltpunkte. Bei einem Spannungswert
529
+ stand er auch — und ergab dort keinen Sinn. */
530
+ if ((s.typ || '') === 'boolean') {
531
+ fb.appendChild(el('div', 'sugg', tr('detail.formulaHint')));
532
+ }
533
+ zeigeBeide(zeile(tr('detail.formula'), fb), ['f']);
534
+
341
535
  /* --- Quelle schreiben --- */
342
536
  var wf = el('div', 'fields');
343
537
  var selW = el('select', 'tx');
344
- selW.style.width = '260px';
538
+ selW.style.width = '420px';
539
+ selW.style.maxWidth = '100%';
345
540
  /* Leer heisst „schreibt nicht" - und muss auch so heissen. Frueher
346
541
  stand hier „dieselbe wie lesen", also das Gegenteil dessen, was
347
542
  passierte (Ricardo, 25.08.2026). Wer wirklich auf die Lesequelle
@@ -355,34 +550,40 @@ export function detailZeile(e, s, idx) {
355
550
  selW.addEventListener('click', function (ev) { ev.stopPropagation(); });
356
551
  selW.addEventListener('change', function () {
357
552
  s.srcW = (selW.value === '=lesen') ? (s.srcR || '') : selW.value;
553
+ /* Wer hier waehlt, hat die Schreibrichtung entschieden. `wr` stammt
554
+ sonst vom Quellobjekt (`common.write`) und hielt die Zeile fuer den
555
+ Detektor beschreibbar, auch nach „nicht schreiben" — angelegt wird
556
+ aber nach `srcW` (`write: !!s.srcW`). Die Vorschau sagte also
557
+ „Platz bleibt frei", der fertige Alias haette ihn bekommen
558
+ (Ricardo, 19.09.2026, an 3D_Drucker tele.SENSOR). */
559
+ s.wr = !!s.srcW;
358
560
  neu();
359
561
  });
360
562
  var lW = el('label', 'fld');
361
- lW.appendChild(el('span', null, 'Datenpunkt'));
362
563
  lW.appendChild(selW);
363
564
  wf.appendChild(lW);
364
565
  /* Der Hinweis gilt nur, wenn die beiden Quellen wirklich
365
566
  auseinandergehen - bei gleicher Quelle ist nichts getrennt. */
366
- if (s.srcW && s.srcW !== s.srcR) {
367
- wf.appendChild(el('div', 'sugg', tr('detail.separateHint')));
567
+ /* Unter die Kennung, nicht daneben: seit die Auswahl 420 px breit ist,
568
+ brach der Satz um und schob sich mit einer Luecke zwischen Auswahl und
569
+ Kennung (Ricardo, 19.09.2026). */
570
+ var wStapel = mitVollId(wf, s.srcW, zielMarke(s, 'w'));
571
+ /* Nur-lesen-Platz mit Schreibziel: der Detektor sieht es bei der
572
+ Vorgaberolle nicht so streng, gemeint ist es aber so — ein Messwert,
573
+ den jemand setzt, geht ans Geraet. */
574
+ var platzDef = null;
575
+ if (mPl && eigenerPlatz) {
576
+ mPl.states.forEach(function (x) { if (!platzDef && x.name === eigenerPlatz) { platzDef = x; } });
368
577
  }
369
- zeigeBeide(zeile(tr('detail.writesTo'), mitVollId(wf, s.srcW, zielMarke(s, 'w'))), ['srcW']);
370
-
371
- /* --- Leseformel --- */
372
- var fb = el('div');
373
- var iF = el('input', 'tx w');
374
- iF.type = 'text';
375
- iF.value = s.f || '';
376
- iF.placeholder = tr('detail.formulaPlaceholder');
377
- iF.addEventListener('click', function (ev) { ev.stopPropagation(); });
378
- iF.addEventListener('change', function () { s.f = iF.value.trim(); neu(); });
379
- fb.appendChild(iF);
380
- /* Der Hinweis gilt nur fuer Schaltpunkte. Bei einem Spannungswert
381
- stand er auch — und ergab dort keinen Sinn. */
382
- if ((s.typ || '') === 'boolean') {
383
- fb.appendChild(el('div', 'sugg', tr('detail.formulaHint')));
578
+ if (platzDef && platzDef.write === false && s.srcW) {
579
+ wStapel.appendChild(el('div', 'aside w', tr('detail.readOnlySlotWrites', platzDef.name)));
384
580
  }
385
- zeigeBeide(zeile(tr('detail.formula'), fb), ['f']);
581
+ /* Nur, wo wirklich woanders gelesen wird - ohne Lesequelle liest die
582
+ Zeile nirgends, und der Satz stimmte nicht. */
583
+ if (s.srcR && s.srcW && s.srcW !== s.srcR) {
584
+ wStapel.appendChild(el('div', 'sugg', tr('detail.separateHint')));
585
+ }
586
+ zeigeBeide(zeile(tr('detail.writesTo'), wStapel), ['srcW']);
386
587
 
387
588
  if (s.srcW) {
388
589
  var wb = el('div');
@@ -399,17 +600,7 @@ export function detailZeile(e, s, idx) {
399
600
 
400
601
  /* --- Rolle, mit Begründung --- */
401
602
  var rb = el('div');
402
- /* Welcher Platz ist schon vergeben, und von welcher Zeile? Das steht
403
- als Vermerk am Gruppenkopf der Rollenliste. */
404
- var belegtVon = {};
405
- if (e.want) {
406
- var fundR = erkenneEntwurf(e, e.want);
407
- if (fundR.length) {
408
- fundR[0].states.forEach(function (x) {
409
- if (x.id) { belegtVon[x.name] = x.id.slice(e.kanal.length + 1); }
410
- });
411
- }
412
- }
603
+
413
604
  rb.appendChild(rollenFeld({
414
605
  wert: s.role,
415
606
  muster: e.want,
@@ -422,6 +613,9 @@ export function detailZeile(e, s, idx) {
422
613
  die Rolle erneut wechselt. */
423
614
  beiWahl: function (r) {
424
615
  s.role = r;
616
+ /* Eine Rolle von Hand ersetzt eine fruehere Platzwahl — sonst
617
+ stuende ein Hinweis zu einem Platz da, den keiner mehr will. */
618
+ delete s.platzWahl;
425
619
  /* Nur wo die Rolle im Muster genau einen Datentyp zulaesst, gibt
426
620
  es etwas vorzugeben. Trifft sie mehrere Plaetze mit
427
621
  verschiedenen Typen - wie die Fahrtrichtung boolean und number -
@@ -437,22 +631,41 @@ export function detailZeile(e, s, idx) {
437
631
  var treffer = platzFuerRolle(e.want, s.role, s.typ);
438
632
  var erlaubt = typenFuerRolle(e.want, s.role);
439
633
  var hin = el('div', 'sugg');
634
+ /* Hat den Platz schon eine ANDERE Zeile, hilft weder der Typ noch die
635
+ Rolle — es gibt ihn nur einmal. Am KeinJson-Testgeraet hielt
636
+ `cmnd_POWER` SET, und unter `stat_POWER` (Rolle `switch` von
637
+ mqtt-client, Typ string) stand „SET verlangt boolean … so faellt der
638
+ Punkt aus dem Muster" — als muesste man nur den Typ stellen. Richtig
639
+ waere ACTUAL gewesen (Ricardo, 19.09.2026). */
640
+ var besetztVon = treffer && !treffer.multiple && belegtVon[treffer.name] &&
641
+ belegtVon[treffer.name] !== s.n ? belegtVon[treffer.name] : null;
440
642
  if (treffer) {
441
643
  hin.appendChild(document.createTextNode(tr('pattern.fitsOn')));
442
644
  hin.appendChild(el('b', null, String(treffer.role)));
443
645
  hin.appendChild(document.createTextNode(tr('pattern.toSlot', treffer.name, musterName(e.want) || e.want)));
444
- if (erlaubt.length) {
646
+ if (!besetztVon && erlaubt.length) {
445
647
  hin.appendChild(document.createTextNode(
446
648
  tr('pattern.expectsType', erlaubt.join(tr('pattern.typeOr')))));
447
649
  }
448
- } else {
650
+ } else if (!eigenerPlatz) {
449
651
  hin.textContent = tr('pattern.noPlaceLong', musterName(e.want) || e.want);
450
652
  }
451
- rb.appendChild(hin);
653
+ /* Findet die Rollenliste nichts, hat die Zeile aber doch einen Platz,
654
+ steht hier nichts: das sagt schon „Platz im Muster“ oben. Der Fall
655
+ ist der Auffangplatz ACTUAL bei `info` — er verlangt keine Rolle und
656
+ steht deshalb in keiner Rollenliste. Hier stand vorher „kein Platz im
657
+ Information-Muster … Alexa & Co. zählen ihn separat“, direkt unter
658
+ einem Feld, das ACTUAL zeigte (Ricardo, 19.09.2026). */
659
+ if (hin.childNodes.length) { rb.appendChild(hin); }
452
660
  /* Passt der Typ nicht zum Platz, faellt der Punkt aus dem Muster -
453
661
  lautlos, denn die Rolle stimmt ja. Genau das ist am 11.09.2026
454
662
  an einem Tasmota-Stromzaehler passiert (I33). */
455
- if (treffer && s.typ && erlaubt.length && erlaubt.indexOf(s.typ) === -1) {
663
+ /* Gelb wie die Typ-Warnung: in beiden Faellen bekommt die Zeile keinen
664
+ Platz. Als grauer Nachsatz ging es unter (Ricardo, 19.09.2026). */
665
+ if (besetztVon) {
666
+ rb.appendChild(el('div', 'aside w', tr('pattern.slotHeldBy', treffer.name, besetztVon)));
667
+ }
668
+ if (treffer && !besetztVon && s.typ && erlaubt.length && erlaubt.indexOf(s.typ) === -1) {
456
669
  rb.appendChild(el('div', 'aside w',
457
670
  tr('pattern.typeMismatch', treffer.name, erlaubt.join(tr('pattern.typeOr')), s.typ)));
458
671
  }
@@ -523,48 +736,56 @@ export function detailZeile(e, s, idx) {
523
736
 
524
737
  /* --- Rohwert und Ergebnis --- */
525
738
  var a = wertVon(s);
526
- zeile(tr('detail.rawValue'), a.roh === undefined ? tr('detail.rawNothing')
527
- : (typeof a.roh === 'object' ? JSON.stringify(a.roh) : String(a.roh)), 'raw');
528
-
529
- if (s.srcR && !S.objects[s.srcR]) {
530
- var wbox = el('div');
531
- wbox.appendChild(el('span', 'chip bad', tr('detail.sourceGone')));
532
- zeile(tr('detail.attention'), wbox);
739
+ /* Ein Knopf ohne Lesequelle hat nichts anzuzeigen - das ist gewollt
740
+ und kein „liefert nichts" in Rot. */
741
+ if (!s.srcR && knopfZeile(s)) {
742
+ zeile(tr('detail.yields'), el('span', 'chip mut', tr('detail.buttonNoRead')));
743
+ a = null;
533
744
  }
534
-
535
- var box = el('div');
536
- var rv = el('span', 'res', a.ok
537
- ? (typeof a.val === 'object' ? JSON.stringify(a.val) : String(a.val)) + (s.unit ? ' ' + s.unit : '')
538
- : a.txt);
539
- if (!a.ok) { rv.style.color = 'var(--bad)'; }
540
- box.appendChild(rv);
541
- box.appendChild(document.createTextNode(' '));
542
- if (a.ok) {
543
- var t = typeof a.val;
544
- var passt = !s.typ || s.typ === t || s.typ === 'mixed';
545
- box.appendChild(el('span', 'chip ' + (passt ? 'ok' : 'warn'),
546
- (passt ? '✓ ' : '≠ ') + (t === 'number' ? 'Zahl' : (t === 'boolean' ? 'boolean' : t))));
547
- if (a.gewandelt) {
548
- box.appendChild(document.createTextNode(' '));
549
- box.appendChild(el('span', 'chip mut', tr('detail.converted')));
550
- }
551
- if (!passt) {
552
- box.appendChild(document.createTextNode(' '));
553
- box.appendChild(el('span', 'chip warn', tr('detail.expects', s.typ)));
745
+ if (a) {
746
+ zeile(tr('detail.rawValue'), a.roh === undefined ? tr('detail.rawNothing')
747
+ : (typeof a.roh === 'object' ? JSON.stringify(a.roh) : String(a.roh)), 'raw');
748
+
749
+ if (s.srcR && !S.objects[s.srcR]) {
750
+ var wbox = el('div');
751
+ wbox.appendChild(el('span', 'chip bad', tr('detail.sourceGone')));
752
+ zeile(tr('detail.attention'), wbox);
554
753
  }
555
- var ist = s.urId ? S.werte[s.urId] : null;
556
- if (ist && ist.val !== undefined && ist.val !== null) {
557
- var gleich = String(ist.val) === String(a.val);
558
- box.appendChild(document.createTextNode(' '));
559
- box.appendChild(el('span', 'chip ' + (gleich ? 'ok' : 'warn'),
560
- gleich ? tr('detail.matchesState') : tr('detail.stateSays', String(ist.val))));
754
+
755
+ var box = el('div');
756
+ var rv = el('span', 'res', a.ok
757
+ ? (typeof a.val === 'object' ? JSON.stringify(a.val) : String(a.val)) + (s.unit ? ' ' + s.unit : '')
758
+ : a.txt);
759
+ if (!a.ok) { rv.style.color = 'var(--bad)'; }
760
+ box.appendChild(rv);
761
+ box.appendChild(document.createTextNode(' '));
762
+ if (a.ok) {
763
+ var t = typeof a.val;
764
+ var passt = !s.typ || s.typ === t || s.typ === 'mixed';
765
+ box.appendChild(el('span', 'chip ' + (passt ? 'ok' : 'warn'),
766
+ (passt ? '✓ ' : '≠ ') + (t === 'number' ? tr('detail.typeNumber') : t)));
767
+ if (a.gewandelt) {
768
+ box.appendChild(document.createTextNode(' '));
769
+ box.appendChild(el('span', 'chip mut', tr('detail.converted')));
770
+ }
771
+ if (!passt) {
772
+ box.appendChild(document.createTextNode(' '));
773
+ box.appendChild(el('span', 'chip warn', tr('detail.expects', s.typ)));
774
+ }
775
+ var ist = s.urId ? S.werte[s.urId] : null;
776
+ if (ist && ist.val !== undefined && ist.val !== null) {
777
+ var gleich = String(ist.val) === String(a.val);
778
+ box.appendChild(document.createTextNode(' '));
779
+ box.appendChild(el('span', 'chip ' + (gleich ? 'ok' : 'warn'),
780
+ gleich ? tr('detail.matchesState') : tr('detail.stateSays', String(ist.val))));
781
+ }
782
+ } else if (a.fehler) {
783
+ box.appendChild(el('span', 'chip bad', tr('detail.formulaBroken')));
784
+ } else {
785
+ box.appendChild(el('span', 'chip bad', tr('detail.yieldsNothing')));
561
786
  }
562
- } else if (a.fehler) {
563
- box.appendChild(el('span', 'chip bad', tr('detail.formulaBroken')));
564
- } else {
565
- box.appendChild(el('span', 'chip bad', tr('detail.yieldsNothing')));
787
+ zeile(tr('detail.yields'), box);
566
788
  }
567
- zeile(tr('detail.yields'), box);
568
789
 
569
790
  if (s.manuell) {
570
791
  var db = el('div');