iobroker.alias-workbench 0.9.9 → 0.9.10
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/LICENSE +1 -1
- package/README.md +15 -0
- package/admin/css/werkbank.css +8 -3
- package/admin/js/entwurf.js +47 -17
- package/admin/js/ergebnis.js +49 -4
- package/admin/js/erkennung.js +46 -2
- package/admin/js/musterwahl.js +12 -2
- package/admin/js/quellentausch.js +179 -19
- package/admin/js/schreiben.js +98 -3
- package/admin/js/sprache.js +6 -1
- package/admin/js/vorlagen.js +12 -2
- package/admin/js/vorlagenblatt.js +65 -3
- package/admin/js/werte.js +63 -1
- package/admin/js/zuordnung.js +29 -35
- package/admin/js/zustandsliste.js +37 -4
- package/admin/sprachen/de.json +13 -0
- package/admin/sprachen/en.json +13 -0
- package/admin/vorlagen/messpunkt.json +2 -1
- package/admin/vorlagen/tasmota-lampe.json +2 -1
- package/admin/vorlagen/tasmota-mehrfach.json +2 -1
- package/admin/vorlagen/tasmota-rgb.json +2 -1
- package/admin/vorlagen/tasmota-steckdose.json +2 -1
- package/io-package.json +14 -14
- package/package.json +1 -1
package/LICENSE
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
MIT License
|
|
2
2
|
|
|
3
|
-
Copyright (c) 2026 Ricardo Hipp
|
|
3
|
+
Copyright (c) 2026 Ricardo Hipp <ricardo.hipp@googlemail.com>
|
|
4
4
|
|
|
5
5
|
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
6
6
|
of this software and associated documentation files (the "Software"), to deal
|
package/README.md
CHANGED
|
@@ -564,6 +564,21 @@ imported, and that both language files carry the same keys.
|
|
|
564
564
|
|
|
565
565
|
## Changelog
|
|
566
566
|
|
|
567
|
+
### 0.9.10 (2026-09-13)
|
|
568
|
+
* Four things from the full test run. **A channel identifier** was not sanitised in the multi-output path: from the channel name “Licht, Bar” the js-controller quietly made `Licht_ Bar`, while the workbench displayed “Licht, Bar” and wrote it into the enumerations — an identifier that never existed, and after deleting the alias it stayed there as a leftover. `ausgangName` now sanitises itself, and where two channels produce the same name the second gets a `_2`. **The caption** differed between the two views: where the point's name equals the row name, “Edit alias” showed it and “Create alias” an empty field. **The row list** now counts like the tree — `POWER2` before `POWER10`. And **an enumeration the workbench created itself** now also disappears when it becomes empty through a change of function, not only when the alias is deleted
|
|
569
|
+
* Tidying: "turn an alias object into a draft row" was written out five times — each with the same write-source rule and slightly different surroundings. Four bugs in this version came from exactly that: the lost write source on the roller shutter, a switching point that became a read-only one, the missing caption on `alias.0.Solar.Netz`, and a deviation the workbench had produced itself. The reading now lives in one place; what the four callers do with it — compare, create, overwrite — stays with them. Behaviour is unchanged
|
|
570
|
+
* Three small things spotted during the run. **RSSI** was pre-ticked in the five Tasmota and measuring-point templates but not in the eleven Homematic ones — the same device property, two answers depending on the vendor; it is now unticked everywhere, like the rest of the diagnostics. **The `MAP_URL` slot** in the vacuum pattern got no role because its expression starts without `^` and the derivation demanded both anchors; a missing start anchor no longer counts against a slot. **And the "guessed" tag** in the swap dialog looked like text stuck to the point name — its styling hung on three parent levels that do not exist there. It is now a pill like in the state list and says in its tooltip whether the guess came from the point name or from the role
|
|
571
|
+
* The save button in the template dialog now also checks when pressed, not only when drawn. It is re-evaluated when a field is left — typing an open bracket into the name hint and clicking "Save template" without leaving the field left you with a blue button and saved a template with a broken expression. The five lock reasons now live in one place that drawing and saving share
|
|
572
|
+
* Three places now say why they cannot. **The save button in the template dialog** was greyed out and silent — five reasons can lock it and none of them was shown; every applicable one now appears on the button and as a box in the dialog. **The error dialog when deleting** only gave a count ("1 operations failed") and not which id is stuck; writing has had that list all along. **And the reason a send point could not be created** hung on a button that the same callback threw away 120 ms later — it now sits at the top of the dry run and survives the rebuild
|
|
573
|
+
* If a point's type does not match its pattern slot, all three places now say so — not just the expanded row. If the slot was **required**, the pattern stops applying altogether, and the whole list showed up tinted: on the Bastelzimmer_Licht, switching SET from `boolean` to `mixed` was enough for eight of eight rows to count as "no slot", while the pattern field reported "✕ SET missing" — SET was right there. It now reads "✕ SET: type mixed instead of boolean", and above the list: "The light pattern does not apply right now: SET requires boolean, but mixed is set. That is why no row has a slot." Only the row that causes it is tinted now — the whole list used to light up, which showed the consequence instead of the cause.
|
|
574
|
+
* A late answer to `system.config` now really does carry the expert mode over. The tab gives up after 2.5 seconds and continues with the browser language; if the answer arrives later, the language stays as it is (switching after the labels are drawn would be worse), the expert mode is adopted and the tree is redrawn once. That had long been built — but above it sat a guard from the time before, which bailed out in exactly this case. Without the mode, every node under `system.*` and `enum.*` was missing, with nothing saying why
|
|
575
|
+
* The swap dialog notices when one level up fits better. The picker lets you choose any node with data points underneath — it has to, because on a flat adapter the channel is the device. With MQTT, though, `stat`, `tele` and `cmnd` are topic folders: landing there finds fewer points, and the lock would ask you to remove some. The workbench now also computes the parent level and says so — "One level up (…) finds a target for 7 of 7 points, here only 5" — with a button that switches the selection. Nothing is forbidden
|
|
576
|
+
* The device list in the swap dialog keeps its position. Scrolling down a long list and picking a device used to jump back to the top, with the freshly picked entry out of sight — picking rebuilds the dialog, and the tree is a different element afterwards. If the selected row still ends up outside, it is brought into view. A new search and a freshly opened dialog still start at the top
|
|
577
|
+
* When swapping the source, a write target no longer counts if it coincides with the read source. The search tries the bare point name as its last form, so `cmnd.POWER` found a status point that also ends in `POWER` — the alias would then have written to its own read source. Guessing already caught this, matching by path did not; it showed when the new source was picked as a sub-node instead of the device
|
|
578
|
+
* When swapping the source, a point that cannot come along now stops the swap instead of letting it fail halfway. A point with separate sources — reads from `stat.POWER`, writes to `cmnd.POWER` — needs both at the new source. If the write target is missing, ioBroker has **no** form that means "reads, writes nowhere": a plain id means "write to the status point", `{read: X}` without `write` is rejected by the js-controller, and `common.write: false` does not protect the source (measured: writing to such an alias overwrote the read source). So the user decides — the checkbox "remove this point when swapping" now appears in this case as well, and "Swap now" stays disabled until someone has decided; the reason shows on the button and as a line in the dialog. Before, the swap started, failed at the controller and left the alias half on the old and half on the new source
|
|
579
|
+
* If a write fails during the swap, **nothing** is deleted. The deletions used to run anyway, so an alias that was already half migrated lost points on top of it. Moving an alias has followed that rule for a long time
|
|
580
|
+
* "All data points of the device" no longer moves the storage location. For a device with several outputs the workbench creates a folder with one channel per output. The button rebuilds the draft and passed on only the **finished target**; `setzeZiel` reads its last segment as the device name — with several outputs that segment is the output, and it ended up in the folder as well. Every press pushed the location one level deeper, and that is where it was written, too: three clicks produced four folders instead of one (`…Garten-Ventilinsel.POWER2.POWER2.POWER2.POWER2`). The button now passes folder and name separately, the way the output selector next to it has always done. A single-output device was never affected
|
|
581
|
+
|
|
567
582
|
### 0.9.9
|
|
568
583
|
* The build source moved from `src/` to `src-admin/`. ioBroker reserves that name for frontend sources that are bundled into `admin/` and never reach the user, and the adapter checker skips such directories when it looks for missing dependencies. `@iobroker/type-detector` therefore stays in `devDependencies`, which is where it belongs: esbuild bakes it into `admin/detector.js` at build time and nothing loads it at runtime. Users receive the finished 71 KB file, not the 548 KB package (W5042)
|
|
569
584
|
* The settings page now takes its translations from `admin/i18n/` — the dictionary ioBroker reads itself (`"i18n": true`) and that Weblate can work with. The eleven languages it already carried moved there unchanged; not a word was retranslated. The tab keeps its own dictionary, now at `admin/sprachen/`: 588 keys in German and English, with an unknown language falling back to English. Two dictionaries, two jobs — the name `i18n` is the one the adapter checker inspects, and it expects all eleven languages there (W5603, W5614, W5015)
|
package/admin/css/werkbank.css
CHANGED
|
@@ -553,9 +553,14 @@
|
|
|
553
553
|
.erow .nm3 .geae { font-family:var(--sans); font-size:8px; font-weight:700; letter-spacing:.05em;
|
|
554
554
|
text-transform:uppercase; background:var(--warn-soft); color:var(--warn);
|
|
555
555
|
padding:0 6px; border-radius:999px; margin-left:6px; }
|
|
556
|
-
|
|
557
|
-
|
|
558
|
-
|
|
556
|
+
/* Die Marke gilt ueberall gleich, nicht nur in der Zustandsliste.
|
|
557
|
+
Mit den drei Ebenen davor griff sie im Tausch-Dialog nicht: dort
|
|
558
|
+
haengt das span unter .sn, und "vermutet" stand als blosser Text
|
|
559
|
+
am Punktnamen - ohne Abstand, ohne Farbe, in 12 statt 8 px
|
|
560
|
+
(Ricardo, 12.09.2026, an alias.0.Garten.FK_Badezimmer). */
|
|
561
|
+
.rat { font-family:var(--sans); font-size:8px; font-weight:700; letter-spacing:.05em;
|
|
562
|
+
text-transform:uppercase; background:var(--accent-soft); color:var(--accent);
|
|
563
|
+
padding:0 6px; border-radius:999px; margin-left:6px; white-space:nowrap; }
|
|
559
564
|
.erow .nm3 .ratknopf { font-family:var(--sans); font-size:11px; cursor:pointer; margin-left:5px;
|
|
560
565
|
color:var(--ink-3); padding:0 4px; border-radius:var(--r-sm); }
|
|
561
566
|
.erow .nm3 .ratknopf:hover { background:var(--surface-3); color:var(--ink); }
|
package/admin/js/entwurf.js
CHANGED
|
@@ -9,7 +9,7 @@ import { txt, tr } from './sprache.js';
|
|
|
9
9
|
import './enums.js';
|
|
10
10
|
import { zusatzName, setzeModus, zeichneBaum, merkeKlappstand } from './baum.js';
|
|
11
11
|
import { kindZustaende, aliasQuellen, holeWerte,
|
|
12
|
-
mitVorspann, schreibQuelle
|
|
12
|
+
mitVorspann, schreibQuelle, zeileAusAlias
|
|
13
13
|
} from './werte.js';
|
|
14
14
|
import { erkenneEntwurf } from './erkennung.js';
|
|
15
15
|
import { vorschlag , pruefeVorlage, zeigeAbozahl } from './vorlagen.js';
|
|
@@ -58,6 +58,17 @@ export function merkeHakenVorgabe(e) {
|
|
|
58
58
|
return e;
|
|
59
59
|
}
|
|
60
60
|
|
|
61
|
+
/* Was von `common.name` als Beschriftung uebrig bleibt.
|
|
62
|
+
|
|
63
|
+
Gleicht der Name dem Zeilennamen, ist er keine eigene Beschriftung,
|
|
64
|
+
sondern die Vorgabe - dann bleibt das Feld leer. Dieselbe Regel gilt
|
|
65
|
+
in `zeileAusAlias` (werte.js) und damit an allen vier Stellen, die
|
|
66
|
+
eine Zeile aus einem Alias-Objekt bauen. */
|
|
67
|
+
function beschriftungAus(name, zeilenname) {
|
|
68
|
+
var nm = txt(name);
|
|
69
|
+
return (nm && nm !== zeilenname) ? nm : '';
|
|
70
|
+
}
|
|
71
|
+
|
|
61
72
|
export function baueEntwurf(kanal) {
|
|
62
73
|
var istAlias = (kanal.indexOf('alias.') === 0);
|
|
63
74
|
var e = { kanal: kanal, states: [], want: null, wantAuto: null, alleMuster: false };
|
|
@@ -106,7 +117,18 @@ export function baueEntwurf(kanal) {
|
|
|
106
117
|
srcW: istAlias ? schreibQuelle(o) : (c.write ? id : ''),
|
|
107
118
|
f: (typeof a.read === 'string') ? a.read : '',
|
|
108
119
|
fw: (typeof a.write === 'string') ? a.write : '',
|
|
109
|
-
|
|
120
|
+
/* Dieselbe Regel wie ueberall sonst: „leer, wenn sie dem
|
|
121
|
+
Zeilennamen gleicht".
|
|
122
|
+
|
|
123
|
+
Hier stand `istAlias ? txt(c.name) : …` ohne diese Pruefung.
|
|
124
|
+
Folge: Wo `common.name` dem Punktnamen gleicht - bei fast jedem
|
|
125
|
+
Alias, den die Werkbank selbst angelegt hat -, zeigte „Alias
|
|
126
|
+
bearbeiten" den Namen und „Alias anlegen" ein leeres Feld.
|
|
127
|
+
Derselbe Alias, zwei Ansichten, zwei Antworten (U21, gemessen
|
|
128
|
+
12.09.2026). Geschrieben wurde in beiden Faellen dasselbe; zu
|
|
129
|
+
sehen war es trotzdem. */
|
|
130
|
+
caption: istAlias ? beschriftungAus(c.name, kurzName)
|
|
131
|
+
: kanalBeschriftung(kanal, kurzName),
|
|
110
132
|
manuell: false
|
|
111
133
|
});
|
|
112
134
|
});
|
|
@@ -1167,18 +1189,33 @@ export function bestandVorrang(e, aliasId) {
|
|
|
1167
1189
|
her, nachdem der Bestand ihn ueberschrieben hat. */
|
|
1168
1190
|
on: s.on
|
|
1169
1191
|
};
|
|
1192
|
+
var ist = zeileAusAlias(o, s.n);
|
|
1193
|
+
/* Rolle und Typ NUR, wenn sie im Alias ueberhaupt stehen.
|
|
1194
|
+
|
|
1195
|
+
`zeileAusAlias` liefert an dieser Stelle `''` - fuer die anderen
|
|
1196
|
+
drei Aufrufer richtig, hier nicht: ein Alias ohne eigene Rolle
|
|
1197
|
+
soll die der Vorlage behalten, statt sie gegen einen Leerstring zu
|
|
1198
|
+
tauschen. Das ist der eine Unterschied, der beim Zusammenlegen der
|
|
1199
|
+
fuenf Stellen erhalten bleiben musste (Y5) - wer ihn wegraeumt,
|
|
1200
|
+
nimmt jedem vorlagenbasierten Alias seine Rollen. */
|
|
1170
1201
|
if (c.role !== undefined) { s.role = c.role; }
|
|
1171
1202
|
if (c.type !== undefined) { s.typ = c.type; }
|
|
1172
|
-
s.unit =
|
|
1203
|
+
s.unit = ist.unit;
|
|
1173
1204
|
s.states = c.states;
|
|
1174
1205
|
/* Die Beschriftung gehoert dem Nutzer - sie wird auch spaeter vom
|
|
1175
|
-
Vorlagen-Abgleich nicht angefasst (Ricardo, 25.08.2026).
|
|
1176
|
-
|
|
1177
|
-
|
|
1178
|
-
|
|
1179
|
-
|
|
1206
|
+
Vorlagen-Abgleich nicht angefasst (Ricardo, 25.08.2026).
|
|
1207
|
+
|
|
1208
|
+
Seit dem 12.09.2026 gilt hier dieselbe Regel wie ueberall sonst:
|
|
1209
|
+
gleicht sie dem Zeilennamen, bleibt das Feld leer. Vorher stand
|
|
1210
|
+
hier `if (c.name !== undefined) s.caption = txt(c.name)` - und
|
|
1211
|
+
weil `vomAliasUebernehmen` danach laeuft und verwirft, hing es
|
|
1212
|
+
an der Reihenfolge, welche der beiden Antworten man zu sehen
|
|
1213
|
+
bekam (U21). */
|
|
1214
|
+
if (c.name !== undefined) { s.caption = ist.caption; }
|
|
1215
|
+
s.f = ist.f;
|
|
1216
|
+
s.fw = ist.fw;
|
|
1180
1217
|
if (q.read) { s.srcR = q.read; }
|
|
1181
|
-
s.srcW =
|
|
1218
|
+
s.srcW = ist.srcW;
|
|
1182
1219
|
s.on = true;
|
|
1183
1220
|
s.ausBestand = true;
|
|
1184
1221
|
});
|
|
@@ -1209,14 +1246,7 @@ export function bestandsAbweichung(s, aliasId) {
|
|
|
1209
1246
|
if (!s || !s.n || !s.on || !aliasId) { return []; }
|
|
1210
1247
|
var o = S.objects[aliasId + '.' + s.n];
|
|
1211
1248
|
if (!o || !o.common) { return []; }
|
|
1212
|
-
var
|
|
1213
|
-
var ist = {
|
|
1214
|
-
role: c.role || '', typ: c.type || '', unit: c.unit || '',
|
|
1215
|
-
f: (typeof a.read === 'string') ? a.read : '',
|
|
1216
|
-
fw: (typeof a.write === 'string') ? a.write : '',
|
|
1217
|
-
srcR: q.read || '',
|
|
1218
|
-
srcW: schreibQuelle(o)
|
|
1219
|
-
};
|
|
1249
|
+
var ist = zeileAusAlias(o, s.n);
|
|
1220
1250
|
var raus = [];
|
|
1221
1251
|
['role', 'typ', 'unit', 'f', 'fw', 'srcR', 'srcW'].forEach(function (k) {
|
|
1222
1252
|
var jetzt = s[k] || '';
|
package/admin/js/ergebnis.js
CHANGED
|
@@ -705,6 +705,27 @@ var warumAuf = false;
|
|
|
705
705
|
neuE.roh = rohJetzt;
|
|
706
706
|
neuE.vorschlag = true;
|
|
707
707
|
neuE.tplName = e.tplName; neuE.grund = e.grund; neuE.ziel = e.ziel;
|
|
708
|
+
/* Ordner und Name getrennt mitgeben, nicht nur das fertige Ziel.
|
|
709
|
+
|
|
710
|
+
`setzeZiel` rechnet den Ordner nur aus, solange keiner dasteht —
|
|
711
|
+
und dabei liest es das letzte Segment von `e.ziel` als
|
|
712
|
+
Geraetenamen. Bei einem Geraet mit mehreren Ausgaengen ist das
|
|
713
|
+
letzte Segment aber der AUSGANG (`…Garten-Ventilinsel.POWER2`),
|
|
714
|
+
und der Ordner bekam ihn angehaengt. Jeder Druck auf diesen
|
|
715
|
+
Knopf schob den Ablageort eine Ebene tiefer, und geschrieben
|
|
716
|
+
wurde er auch so: gemessen 12.09.2026 am Testsystem entstanden
|
|
717
|
+
nach drei Klicks vier Ordner statt einem
|
|
718
|
+
(`…Garten-Ventilinsel.POWER2.POWER2.POWER2.POWER2`).
|
|
719
|
+
|
|
720
|
+
Der Ausgangswechsel eine Stelle weiter oben macht es seit jeher
|
|
721
|
+
richtig — hier dieselbe Form. Im Aliasmodus sind beide Felder
|
|
722
|
+
`undefined`; dann bleibt alles wie bisher, `setzeZiel` steigt
|
|
723
|
+
dort ohnehin sofort aus. `ordnerVonHand` gehoert mit, sonst
|
|
724
|
+
verliert der Raumvorschlag die Entscheidung, wer den Ordner
|
|
725
|
+
gesetzt hat (E41). */
|
|
726
|
+
neuE.zielOrdner = e.zielOrdner;
|
|
727
|
+
neuE.zielName = e.zielName;
|
|
728
|
+
neuE.ordnerVonHand = e.ordnerVonHand;
|
|
708
729
|
if (fest) { neuE.vonHandGewaehlt = false; neuE.ausBestand = true; }
|
|
709
730
|
if (vorh && S.objects[vorh]) { bestandVorrang(neuE, vorh); }
|
|
710
731
|
S.entwurf = neuE; S.openRow = null; zeichneErgebnis();
|
|
@@ -827,15 +848,32 @@ var warumAuf = false;
|
|
|
827
848
|
ba.hidden = !mehr && !mehrK;
|
|
828
849
|
if (mehr) {
|
|
829
850
|
ba.textContent = tr('write.createAll', e.instanzen.length);
|
|
851
|
+
/* Dieselbe Rechnung wie beim Schreiben, samt Kollisionsschutz.
|
|
852
|
+
|
|
853
|
+
Zwei Kanaele koennen nach der Saeuberung denselben Namen
|
|
854
|
+
ergeben („Licht Bar" und „Licht, Bar" beide `Licht_Bar`); der
|
|
855
|
+
Schreibpfad haengt dann `_2` an. Ohne dieselbe Rechnung stuende
|
|
856
|
+
im Hinweis zweimal derselbe Name und danach etwas anderes am
|
|
857
|
+
System - genau die Luecke, die G31 beschreibt (12.09.2026). */
|
|
858
|
+
var belegt = {};
|
|
830
859
|
ba.title = e.instanzen.map(function (n) {
|
|
831
|
-
|
|
860
|
+
var nm = (String(n) === String(e.instanz)) ? e.zielName : ausgangName(e, n);
|
|
861
|
+
if (belegt[nm]) {
|
|
862
|
+
var z = 1, n2;
|
|
863
|
+
do { z++; n2 = nm + '_' + z; } while (belegt[n2]);
|
|
864
|
+
nm = n2;
|
|
865
|
+
}
|
|
866
|
+
belegt[nm] = 1;
|
|
867
|
+
return (e.zielOrdner || '') + '.' + nm;
|
|
832
868
|
}).join(String.fromCharCode(10));
|
|
833
869
|
} else if (mehrK) {
|
|
834
870
|
ba.textContent = tr('write.createPerChannel', e.kanalGeraete.length);
|
|
835
871
|
/* Im Hinweis stehen die Namen, die entstehen wuerden — sonst
|
|
836
872
|
klickt man auf eine Zahl und weiss nicht, was danach dasteht. */
|
|
837
873
|
ba.title = e.kanalGeraete.map(function (k) {
|
|
838
|
-
|
|
874
|
+
/* Die Kennung, nicht den Anzeigenamen - so heisst das Objekt
|
|
875
|
+
hinterher wirklich (G31). */
|
|
876
|
+
return (e.zielOrdner || '') + '.' + (k.kennung || k.name) + ' (' + k.typ + ')';
|
|
839
877
|
}).join(String.fromCharCode(10));
|
|
840
878
|
}
|
|
841
879
|
ba.disabled = false;
|
|
@@ -1050,9 +1088,16 @@ var warumAuf = false;
|
|
|
1050
1088
|
function verkabeleDialoge() {
|
|
1051
1089
|
var bc = $('#btn-checks'), bd = $('#btn-dry'), bw = $('#btn-dry-write');
|
|
1052
1090
|
if (bc) { bc.addEventListener('click', function () { $('#dlg-checks').showModal(); }); }
|
|
1053
|
-
|
|
1091
|
+
/* Beim Oeffnen von Hand faengt der Dialog frisch an - ein alter
|
|
1092
|
+
Sendepunkt-Fehler von vorhin hat dann nichts mehr zu sagen (H17).
|
|
1093
|
+
Aus dem Rueckruf heraus wird zeigeTrockenlauf direkt gerufen, dort
|
|
1094
|
+
bleibt die Meldung also stehen. */
|
|
1095
|
+
var frisch = function (alle) {
|
|
1096
|
+
return function () { S.mqFehler = null; zeigeTrockenlauf(alle); };
|
|
1097
|
+
};
|
|
1098
|
+
if (bd) { bd.addEventListener('click', frisch(false)); }
|
|
1054
1099
|
var ba2 = $('#btn-dry-alle');
|
|
1055
|
-
if (ba2) { ba2.addEventListener('click',
|
|
1100
|
+
if (ba2) { ba2.addEventListener('click', frisch(true)); }
|
|
1056
1101
|
if (bw) { bw.addEventListener('click', schreibeObjekte); }
|
|
1057
1102
|
var bdel = $('#btn-del'), bdg = $('#btn-del-go');
|
|
1058
1103
|
if (bdel) { bdel.addEventListener('click', zeigeLoeschen); }
|
package/admin/js/erkennung.js
CHANGED
|
@@ -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.
|
|
68
|
-
|
|
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/musterwahl.js
CHANGED
|
@@ -10,7 +10,7 @@
|
|
|
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
15
|
import { ratePlaetze, rateZurueck, rateAnzahl, rateMoeglich } from './vorschlagen.js';
|
|
16
16
|
import { zeichneErgebnis, entwurfAngefasst, knopfFrisch } from './ergebnis.js';
|
|
@@ -138,7 +138,17 @@ export function baueMusterwahl(e, haupt, pflichtFehlt) {
|
|
|
138
138
|
if (!r.length) {
|
|
139
139
|
var fehlend = musterVon(m).states.filter(function (x) { return x.required; })
|
|
140
140
|
.map(function (x) { return x.name; });
|
|
141
|
-
|
|
141
|
+
/* Ein Pflichtplatz kann auch deshalb leer sein, weil die Zeile
|
|
142
|
+
dafuer im falschen Datentyp steht - die Rolle stimmt, der Typ
|
|
143
|
+
nicht, und der Punkt faellt aus dem Muster. „SET fehlt" ist dann
|
|
144
|
+
die falsche Auskunft: SET ist da. Der Typ-Grund geht vor, wo es
|
|
145
|
+
einen gibt (Ricardo, 12.09.2026, am Bastelzimmer_Licht). */
|
|
146
|
+
var konflikt = typKonflikte(pruefE.states, m)
|
|
147
|
+
.filter(function (x) { return fehlend.indexOf(x.platz) > -1; })[0];
|
|
148
|
+
if (konflikt) {
|
|
149
|
+
mark = tr('pattern.fitsNotType', konflikt.platz, konflikt.typ,
|
|
150
|
+
konflikt.erwartet.join(tr('pattern.typeOr')));
|
|
151
|
+
} else if (fehlend.length) {
|
|
142
152
|
mark = '✕ ' + tr('pattern.missing', fehlend.join(', '));
|
|
143
153
|
} else {
|
|
144
154
|
/* Kein Pflichtplatz fehlt und trotzdem passt nichts - dann liegt
|
|
@@ -92,6 +92,15 @@ function ausVorlage(punktId, neuKanal) {
|
|
|
92
92
|
|
|
93
93
|
/* Stufe 3: gleicher Punktname irgendwo unter dem neuen Geraet, oder
|
|
94
94
|
gleiche Rolle. Bewusst schwach - deshalb nur als Vorschlag. */
|
|
95
|
+
/* Und WORUEBER geraten wurde, wird mitgegeben.
|
|
96
|
+
|
|
97
|
+
„vermutet" allein sagt nur, dass die Zuordnung schwach ist - nicht,
|
|
98
|
+
woran sie haengt. Ueber den Namen geraten heisst: irgendein Punkt
|
|
99
|
+
heisst hinten gleich; das kann weit danebenliegen (ein Fensterkontakt
|
|
100
|
+
`…1.STATE` landete so auf `…tele.STATE`, dem Telemetrie-JSON einer
|
|
101
|
+
Tasmota). Ueber die Rolle geraten ist etwas anderes und muss anders
|
|
102
|
+
geprueft werden. Der Tooltip an der Marke sagt es jetzt (Ricardo,
|
|
103
|
+
12.09.2026). */
|
|
95
104
|
function geraten(altQuelle, neuKanal) {
|
|
96
105
|
var name = altQuelle.split('.').pop();
|
|
97
106
|
var kandidaten = kindZustaende(neuKanal);
|
|
@@ -99,7 +108,7 @@ function geraten(altQuelle, neuKanal) {
|
|
|
99
108
|
kandidaten.forEach(function (id) {
|
|
100
109
|
if (!treffer && id.split('.').pop() === name) { treffer = id; }
|
|
101
110
|
});
|
|
102
|
-
if (treffer) { return treffer; }
|
|
111
|
+
if (treffer) { return { id: treffer, ueber: 'name' }; }
|
|
103
112
|
var altO = S.objects[altQuelle];
|
|
104
113
|
var rolle = altO && altO.common && altO.common.role;
|
|
105
114
|
if (rolle) {
|
|
@@ -108,7 +117,7 @@ function geraten(altQuelle, neuKanal) {
|
|
|
108
117
|
if (!treffer && o && o.common && o.common.role === rolle) { treffer = id; }
|
|
109
118
|
});
|
|
110
119
|
}
|
|
111
|
-
return treffer;
|
|
120
|
+
return treffer ? { id: treffer, ueber: 'rolle', rolle: rolle } : null;
|
|
112
121
|
}
|
|
113
122
|
|
|
114
123
|
/* Die Zuordnungstabelle: je Alias-Punkt, was heute gilt und was danach
|
|
@@ -158,10 +167,14 @@ export function tauschPlan(kanal, neuKanal) {
|
|
|
158
167
|
/* 3. geraten */
|
|
159
168
|
if (!zeile.neuR && zeile.altR) {
|
|
160
169
|
var g = geraten(zeile.altR, neuKanal);
|
|
161
|
-
if (g) {
|
|
170
|
+
if (g) {
|
|
171
|
+
zeile.neuR = g.id; zeile.stufe = 3;
|
|
172
|
+
zeile.geratenUeber = g.ueber; zeile.geratenRolle = g.rolle || '';
|
|
173
|
+
}
|
|
162
174
|
}
|
|
163
175
|
if (!zeile.neuW && zeile.altW) {
|
|
164
|
-
var
|
|
176
|
+
var gwT = geraten(zeile.altW, neuKanal);
|
|
177
|
+
var gw = gwT && gwT.id;
|
|
165
178
|
/* Was hier geraten wird, muss ein anderer Punkt sein als die
|
|
166
179
|
Lesequelle. Bei getrennten Quellen heissen Melder und Befehl oft
|
|
167
180
|
gleich - `stat.POWER` und `cmnd.POWER` -, und das Raten greift
|
|
@@ -172,6 +185,21 @@ export function tauschPlan(kanal, neuKanal) {
|
|
|
172
185
|
(gemessen 09.09.2026). Lieber kein Treffer als dieser. */
|
|
173
186
|
if (gw && gw !== zeile.neuR) { zeile.neuW = gw; }
|
|
174
187
|
}
|
|
188
|
+
/* Bei getrennten Quellen darf das Schreibziel nie die Lesequelle
|
|
189
|
+
sein - egal, aus welcher der drei Stufen es kam.
|
|
190
|
+
|
|
191
|
+
Beim Raten stand diese Regel schon (siehe oben), beim PFADTREFFER
|
|
192
|
+
fehlte sie: `suchePfad` probiert als letzte Form den blossen
|
|
193
|
+
Punktnamen, und `cmnd.POWER` findet so `…Stumm.stat.POWER` - den
|
|
194
|
+
Melder. Sichtbar wird es, wenn man beim Tauschen nicht das Geraet,
|
|
195
|
+
sondern einen Unterknoten waehlt (`…Stumm.stat` statt `…Stumm`):
|
|
196
|
+
dann verschwand der rote Hinweis, der Knopf wurde frei, und
|
|
197
|
+
geschrieben haette der Alias auf seine eigene Lesequelle - genau
|
|
198
|
+
der Fehler, den W20 abschaffen sollte (Ricardo, 12.09.2026).
|
|
199
|
+
|
|
200
|
+
Danach steht `neuW` leer, und die Sperre aus W20 greift: der
|
|
201
|
+
Nutzer entscheidet, ob der Punkt entfernt wird. */
|
|
202
|
+
if (!zeile.einfach && zeile.neuW && zeile.neuW === zeile.neuR) { zeile.neuW = ''; }
|
|
175
203
|
/* Bei einfacher Quelle gilt die Lesequelle fuer beides. */
|
|
176
204
|
if (zeile.einfach && zeile.neuR) { zeile.neuW = zeile.neuR; }
|
|
177
205
|
return zeile;
|
|
@@ -339,6 +367,25 @@ function zeichneAuswahlbaum(host, filter, beiWahl) {
|
|
|
339
367
|
if (!host.childNodes.length) {
|
|
340
368
|
host.appendChild(el('div', 'empty', tr('swap.noDevice')));
|
|
341
369
|
}
|
|
370
|
+
|
|
371
|
+
/* Die Liste bleibt stehen, wo sie stand.
|
|
372
|
+
|
|
373
|
+
Beim Aufklappen fiel das nicht auf: derselbe Behaelter, nur neu
|
|
374
|
+
gefuellt, und `scrollTop` ueberlebt das. Beim WAEHLEN schon -
|
|
375
|
+
`zeichneTausch` baut den ganzen Dialog neu, der Baum ist danach ein
|
|
376
|
+
anderes Element und faengt bei 0 an. Wer weit unten ein Geraet
|
|
377
|
+
anklickte, sah danach den Anfang der Liste und seinen eben
|
|
378
|
+
gewaehlten Eintrag gar nicht mehr (Ricardo, 12.09.2026, an einer
|
|
379
|
+
Liste mit 42 Zeilen: scrollTop 700 -> 0).
|
|
380
|
+
|
|
381
|
+
Gemerkt wird an `S`, nicht am Element: das Element gibt es beim
|
|
382
|
+
naechsten Zeichnen nicht mehr. `block: 'nearest'` schiebt nur, wenn
|
|
383
|
+
die Zeile wirklich draussen ist - sonst springt die Liste bei jedem
|
|
384
|
+
Klick ein Stueck, und das waere die naechste Unruhe. */
|
|
385
|
+
if (S.tauschScroll) { host.scrollTop = S.tauschScroll; }
|
|
386
|
+
var gewaehlteZeile = host.querySelector('.node.sel');
|
|
387
|
+
if (gewaehlteZeile) { gewaehlteZeile.scrollIntoView({ block: 'nearest' }); }
|
|
388
|
+
host.addEventListener('scroll', function () { S.tauschScroll = host.scrollTop; });
|
|
342
389
|
}
|
|
343
390
|
|
|
344
391
|
export function zeigeTausch() {
|
|
@@ -355,6 +402,9 @@ export function zeigeTausch() {
|
|
|
355
402
|
schlimmsten Fall genau dann, wenn im Feld noch das eben getauschte,
|
|
356
403
|
inzwischen geloeschte Geraet stand (W13, gemessen 26.08.2026). */
|
|
357
404
|
S.tauschFilter = '';
|
|
405
|
+
/* Und die Liste faengt oben an - sonst startet der naechste Tausch
|
|
406
|
+
dort, wo der letzte aufgehoert hat. */
|
|
407
|
+
S.tauschScroll = 0;
|
|
358
408
|
var b = $('#btn-swap-go');
|
|
359
409
|
if (b) { b.hidden = false; b.textContent = tr('swap.now'); }
|
|
360
410
|
zeichneTausch();
|
|
@@ -420,6 +470,10 @@ function zeichneTausch() {
|
|
|
420
470
|
zeichneAuswahlbaum(baumHost, S.tauschFilter, gewaehlt);
|
|
421
471
|
feld.addEventListener('input', function () {
|
|
422
472
|
S.tauschFilter = feld.value;
|
|
473
|
+
/* Andere Suche, andere Liste - die alte Scrollhoehe passt nicht mehr
|
|
474
|
+
und zeigte sonst ins Leere. */
|
|
475
|
+
S.tauschScroll = 0;
|
|
476
|
+
baumHost.scrollTop = 0;
|
|
423
477
|
zeichneAuswahlbaum(baumHost, S.tauschFilter, gewaehlt);
|
|
424
478
|
});
|
|
425
479
|
|
|
@@ -434,6 +488,40 @@ function zeichneTausch() {
|
|
|
434
488
|
/* Die Zuordnung, Zeile fuer Zeile. */
|
|
435
489
|
if (!S.tauschPlan) { S.tauschPlan = tauschPlan(kanal, S.tauschNeu); }
|
|
436
490
|
var plan = S.tauschPlan;
|
|
491
|
+
|
|
492
|
+
/* Steht die Wahl eine Ebene zu tief?
|
|
493
|
+
|
|
494
|
+
Der Baum laesst jeden Knoten waehlen, unter dem Datenpunkte haengen -
|
|
495
|
+
absichtlich, denn welche Ebene beim fremden Adapter das Geraet ist,
|
|
496
|
+
kann die Werkbank nicht wissen; bei einem flachen Adapter ist der
|
|
497
|
+
Kanal die richtige (Kommentar in `tauschPlan`). Bei MQTT sind
|
|
498
|
+
`stat`, `tele` und `cmnd` aber Themenordner, keine Geraete, und wer
|
|
499
|
+
dort landet, bekommt schlechtere Treffer: am RGB-Deckenlicht fanden
|
|
500
|
+
ueber das Geraet alle sieben Punkte ein Ziel, ueber `…Licht.tele`
|
|
501
|
+
nur fuenf - die beiden aus `stat` gar nichts (12.09.2026).
|
|
502
|
+
|
|
503
|
+
Verboten wird nichts. Aber die Werkbank kann nachrechnen, was eine
|
|
504
|
+
Ebene hoeher herauskaeme, und es sagen - sonst hakt man zwei Punkte
|
|
505
|
+
zum Entfernen an und opfert seinen Schalter, statt den Knoten
|
|
506
|
+
darueber zu nehmen (Ricardos Frage, 12.09.2026). */
|
|
507
|
+
var eltern = S.tauschNeu.split('.').slice(0, -1).join('.');
|
|
508
|
+
if (eltern.split('.').length > 2 && S.tauschNeu !== alt) {
|
|
509
|
+
var trefferHier = plan.filter(function (z) { return z.neuR; }).length;
|
|
510
|
+
var planOben = tauschPlan(kanal, eltern);
|
|
511
|
+
var trefferOben = planOben.filter(function (z) { return z.neuR; }).length;
|
|
512
|
+
if (trefferOben > trefferHier) {
|
|
513
|
+
var eb = el('div', 'aside w');
|
|
514
|
+
eb.style.borderLeftColor = 'var(--warn)';
|
|
515
|
+
eb.appendChild(el('b', null, tr('swap.higherLevel',
|
|
516
|
+
eltern, trefferOben, plan.length, trefferHier)));
|
|
517
|
+
var knopf = el('button', 'btn mini wichtig', tr('swap.goHigher'));
|
|
518
|
+
knopf.style.marginTop = '5px';
|
|
519
|
+
knopf.addEventListener('click', function () { gewaehlt(eltern); });
|
|
520
|
+
eb.appendChild(el('div', null, ''));
|
|
521
|
+
eb.appendChild(knopf);
|
|
522
|
+
body.appendChild(eb);
|
|
523
|
+
}
|
|
524
|
+
}
|
|
437
525
|
var k = el('div', 'card');
|
|
438
526
|
k.style.marginTop = '11px';
|
|
439
527
|
var ch = el('div', 'ch');
|
|
@@ -460,9 +548,49 @@ function zeichneTausch() {
|
|
|
460
548
|
&& (z.einfach || (z.neuW || '') === (z.altW || ''));
|
|
461
549
|
var nm = el('span', 'sn', z.name);
|
|
462
550
|
if (gleich) { nm.appendChild(el('span', 'rat', tr('swap.unchanged'))); }
|
|
463
|
-
else if (z.stufe === 3) {
|
|
551
|
+
else if (z.stufe === 3) {
|
|
552
|
+
/* Die Marke sagt jetzt auch, WORUEBER geraten wurde - ueber den
|
|
553
|
+
blossen Punktnamen oder ueber die Rolle. Das sind zwei sehr
|
|
554
|
+
verschiedene Vermutungen, und sie pruefen sich verschieden. */
|
|
555
|
+
var rm = el('span', 'rat', tr('guess.mark'));
|
|
556
|
+
rm.title = z.geratenUeber === 'rolle'
|
|
557
|
+
? tr('swap.guessedByRole', z.geratenRolle || '?')
|
|
558
|
+
: tr('swap.guessedByName', (z.altR || '').split('.').pop());
|
|
559
|
+
nm.appendChild(rm);
|
|
560
|
+
}
|
|
464
561
|
r.appendChild(nm);
|
|
465
562
|
var rechts = el('span');
|
|
563
|
+
/* Das Kaestchen „entfernen" - an zwei Stellen gebraucht, deshalb hier.
|
|
564
|
+
|
|
565
|
+
Es gab es bisher nur, wenn GAR NICHTS gefunden wurde. Der zweite
|
|
566
|
+
Fall braucht es genauso: Lesequelle gefunden, Schreibziel nicht.
|
|
567
|
+
Ein Punkt mit Schreibrecht ohne Schreibziel laesst sich in ioBroker
|
|
568
|
+
nicht ausdruecken - jede Form, die der js-controller annimmt,
|
|
569
|
+
schreibt irgendwohin, und `{read: X}` ohne `write` lehnt er ab
|
|
570
|
+
(„The id is empty", gemessen 12.09.2026). Also entscheidet der
|
|
571
|
+
Nutzer: mitnehmen geht nicht, entfernen schon (Ricardo). */
|
|
572
|
+
var entfernenKaestchen = function () {
|
|
573
|
+
var lb = document.createElement('label');
|
|
574
|
+
lb.style.display = 'flex';
|
|
575
|
+
lb.style.gap = '6px';
|
|
576
|
+
lb.style.alignItems = 'center';
|
|
577
|
+
lb.style.marginTop = '3px';
|
|
578
|
+
lb.style.cursor = 'pointer';
|
|
579
|
+
var cb = document.createElement('input');
|
|
580
|
+
cb.type = 'checkbox';
|
|
581
|
+
cb.checked = !!S.tauschWeg[z.id];
|
|
582
|
+
cb.addEventListener('change', function () {
|
|
583
|
+
S.tauschWeg[z.id] = cb.checked;
|
|
584
|
+
/* Der Knopf haengt daran - also neu rechnen, nicht erst beim
|
|
585
|
+
naechsten Oeffnen. `zeichneTausch`, nicht `zeigeTausch`: das
|
|
586
|
+
zweite setzt Auswahl, Plan und Haken zurueck und faengt von
|
|
587
|
+
vorn an. */
|
|
588
|
+
zeichneTausch();
|
|
589
|
+
});
|
|
590
|
+
lb.appendChild(cb);
|
|
591
|
+
lb.appendChild(el('span', 'hint', tr('swap.removeIt')));
|
|
592
|
+
return lb;
|
|
593
|
+
};
|
|
466
594
|
/* Beschriftet, sonst raet man, welche der beiden Zeilen die neue ist. */
|
|
467
595
|
var paarZeile = function (beschriftung, text, klasse, farbe) {
|
|
468
596
|
var zz = el('div');
|
|
@@ -489,6 +617,7 @@ function zeichneTausch() {
|
|
|
489
617
|
kw.style.color = 'var(--bad)';
|
|
490
618
|
kw.textContent = tr('swap.noWriteMatch');
|
|
491
619
|
rechts.appendChild(kw);
|
|
620
|
+
rechts.appendChild(entfernenKaestchen());
|
|
492
621
|
}
|
|
493
622
|
} else {
|
|
494
623
|
var fehlt = el('div');
|
|
@@ -497,19 +626,7 @@ function zeichneTausch() {
|
|
|
497
626
|
rechts.appendChild(fehlt);
|
|
498
627
|
/* Was nichts findet, laesst sich mit entfernen - sonst bliebe ein
|
|
499
628
|
Punkt zurueck, der ins Leere zeigt (Ricardos Wahl, 25.08.2026). */
|
|
500
|
-
|
|
501
|
-
lb.style.display = 'flex';
|
|
502
|
-
lb.style.gap = '6px';
|
|
503
|
-
lb.style.alignItems = 'center';
|
|
504
|
-
lb.style.marginTop = '3px';
|
|
505
|
-
lb.style.cursor = 'pointer';
|
|
506
|
-
var cb = document.createElement('input');
|
|
507
|
-
cb.type = 'checkbox';
|
|
508
|
-
cb.checked = !!S.tauschWeg[z.id];
|
|
509
|
-
cb.addEventListener('change', function () { S.tauschWeg[z.id] = cb.checked; });
|
|
510
|
-
lb.appendChild(cb);
|
|
511
|
-
lb.appendChild(el('span', 'hint', tr('swap.removeIt')));
|
|
512
|
-
rechts.appendChild(lb);
|
|
629
|
+
rechts.appendChild(entfernenKaestchen());
|
|
513
630
|
}
|
|
514
631
|
r.appendChild(rechts);
|
|
515
632
|
k.appendChild(r);
|
|
@@ -556,7 +673,39 @@ function zeichneTausch() {
|
|
|
556
673
|
if (plan.some(function (z) { return z.stufe === 3; })) {
|
|
557
674
|
body.appendChild(el('div', 'aside', tr('swap.guessHint')));
|
|
558
675
|
}
|
|
559
|
-
|
|
676
|
+
|
|
677
|
+
/* Punkte, die nicht mitkoennen, halten den Tausch auf - bis der Nutzer
|
|
678
|
+
sagt, was mit ihnen geschehen soll.
|
|
679
|
+
|
|
680
|
+
Zwei Faelle, beide mit demselben Ausgang (das Kaestchen daneben):
|
|
681
|
+
gar kein Treffer, oder getrennte Quellen mit Lesetreffer und ohne
|
|
682
|
+
Schreibziel. Frueher lief der Tausch in beiden Faellen los. Der
|
|
683
|
+
zweite endete dabei im Fehler des js-controllers, und weil
|
|
684
|
+
`tauscheAus` Objekt fuer Objekt schrieb, stand der Alias danach
|
|
685
|
+
halb auf der alten und halb auf der neuen Quelle (W20, gemessen
|
|
686
|
+
12.09.2026). Der erste lief still durch und liess den Punkt auf der
|
|
687
|
+
alten Quelle stehen - derselbe Mischzustand, nur ohne Meldung.
|
|
688
|
+
|
|
689
|
+
Gesperrt wird mit Begruendung am Knopf, nicht bloss grau: ein
|
|
690
|
+
Knopf, der nicht sagt warum, ist der Fehler aus I32. */
|
|
691
|
+
var offeneStolper = plan.filter(function (z) {
|
|
692
|
+
if (S.tauschWeg[z.id]) { return false; }
|
|
693
|
+
if (!z.neuR) { return true; }
|
|
694
|
+
return !!(z.altW && z.altW !== z.altR && !z.neuW);
|
|
695
|
+
});
|
|
696
|
+
if (go) {
|
|
697
|
+
if (offeneStolper.length) {
|
|
698
|
+
go.disabled = true;
|
|
699
|
+
go.title = tr('swap.blocked', offeneStolper.map(function (z) { return z.name; }).join(', '));
|
|
700
|
+
var sp = el('div', 'aside w');
|
|
701
|
+
sp.style.borderLeftColor = 'var(--bad)';
|
|
702
|
+
sp.appendChild(el('b', null, go.title));
|
|
703
|
+
body.appendChild(sp);
|
|
704
|
+
} else {
|
|
705
|
+
go.disabled = false;
|
|
706
|
+
go.title = '';
|
|
707
|
+
}
|
|
708
|
+
}
|
|
560
709
|
}
|
|
561
710
|
|
|
562
711
|
export function tauscheAus() {
|
|
@@ -585,6 +734,17 @@ export function tauscheAus() {
|
|
|
585
734
|
};
|
|
586
735
|
|
|
587
736
|
var weiter = function () {
|
|
737
|
+
/* Schlug beim Umschreiben etwas fehl, wird nichts geloescht.
|
|
738
|
+
|
|
739
|
+
Dieselbe Regel wie beim Verlegen (T11): erst alles Neue, und geht
|
|
740
|
+
dabei etwas schief, bleibt der Rest unberuehrt. Hier fehlte sie -
|
|
741
|
+
`weiter` loeschte auch dann, wenn die Haelfte der Punkte noch auf
|
|
742
|
+
der alten Quelle stand. Zurueck kann die Werkbank nichts nehmen,
|
|
743
|
+
aber sie kann aufhoeren, es schlimmer zu machen: der Nutzer sieht
|
|
744
|
+
die Fehler und drueckt „Noch einmal versuchen", und die schon
|
|
745
|
+
umgeschriebenen Punkte stoeren dabei nicht - sie stehen ja bereits
|
|
746
|
+
richtig (12.09.2026). */
|
|
747
|
+
if (fehler.length) { return fertig(); }
|
|
588
748
|
if (!loeschen.length) { return fertig(); }
|
|
589
749
|
var offen2 = loeschen.length;
|
|
590
750
|
loeschen.forEach(function (id) {
|