iobroker.alias-workbench 0.8.3 → 0.9.7
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +146 -3
- package/admin/css/werkbank.css +57 -8
- package/admin/detector.js +1 -1
- package/admin/geraetewissen/tasmota-befehle.json +3 -1
- package/admin/i18n/de.json +30 -6
- package/admin/i18n/en.json +30 -6
- package/admin/js/aufzaehlungen.js +11 -1
- package/admin/js/baum.js +118 -37
- package/admin/js/detail.js +76 -9
- package/admin/js/entwurf.js +313 -43
- package/admin/js/enums.js +50 -7
- package/admin/js/ergebnis.js +161 -37
- package/admin/js/erkennung.js +54 -5
- package/admin/js/katalog.js +12 -3
- package/admin/js/mqtt.js +76 -13
- package/admin/js/musternamen.js +91 -26
- package/admin/js/musterwahl.js +10 -0
- package/admin/js/objekte.js +29 -3
- package/admin/js/pruefungen.js +10 -2
- package/admin/js/quellentausch.js +31 -2
- package/admin/js/schreiben.js +209 -24
- package/admin/js/sprache.js +54 -7
- package/admin/js/start.js +32 -2
- package/admin/js/vorlagen.js +114 -6
- package/admin/js/vorlagenblatt.js +195 -26
- package/admin/js/werte.js +113 -6
- package/admin/js/zuordnung.js +71 -17
- package/admin/js/zustand.js +7 -1
- package/admin/js/zustandsliste.js +157 -26
- package/admin/tab.html +25 -3
- package/admin/vorlagen/hm-dimmer.json +1 -1
- package/admin/vorlagen/hm-schaltaktor-lampe.json +2 -2
- package/admin/vorlagen/hm-schaltaktor-mehrfach.json +2 -2
- package/admin/vorlagen/hm-schaltaktor-steckdose.json +2 -2
- package/io-package.json +105 -1
- package/package.json +1 -1
package/admin/js/musternamen.js
CHANGED
|
@@ -1,18 +1,33 @@
|
|
|
1
|
-
/* Die uebersetzten Namen der Muster, angezapft beim
|
|
1
|
+
/* Die uebersetzten Namen der Muster, angezapft beim Admin.
|
|
2
2
|
|
|
3
|
-
Der type-detector kennt nur rohe Typnamen (socket, thermostat)
|
|
4
|
-
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
3
|
+
Der type-detector kennt nur rohe Typnamen (socket, thermostat) und
|
|
4
|
+
bringt selbst KEINE Uebersetzung mit - er ist eine Bibliothek
|
|
5
|
+
(`@iobroker/type-detector`), kein Adapter. Uebersetzt werden seine
|
|
6
|
+
Typen nur in den Sprachdateien der Adapter, die ihn benutzen, und
|
|
7
|
+
jeder schleppt seine eigene Kopie mit. Ein Block je Sprache, jeder
|
|
8
|
+
gibt sich am Eintrag "type-Device type" selbst zu erkennen; wir holen
|
|
9
|
+
den der Oberflaechensprache und schlagen darin nach. Eine eigene
|
|
10
|
+
Liste wuerde bei jedem type-detector-Update altern (Ricardos Punkt 13,
|
|
11
|
+
25.08.2026 - Weg 2, wie der Raum/Funktion-Katalog).
|
|
10
12
|
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
13
|
+
Gefragt wird ausschliesslich der ADMIN. Bis zum 08.09.2026 war es der
|
|
14
|
+
Geraete-Adapter; Ricardos Begruendung fuer den Wechsel: Der Admin ist
|
|
15
|
+
immer installiert, `devices` nicht, und er wird besser gepflegt. Was
|
|
16
|
+
er nicht uebersetzt, bleibt eben roh - gemessen am 08.09.2026:
|
|
17
|
+
Admin 8.0.11 kennt alle 53 Typen, Admin 7.8.23 nur 44.
|
|
18
|
+
|
|
19
|
+
Der Weg dorthin geht ueber zwei Verzeichnisse, weil keines allein
|
|
20
|
+
reicht: `index.html` nennt bei Admin 8 das richtige Buendel, bei
|
|
21
|
+
Admin 7 aber nur vier Bootstrap-Haeppchen, und die fuehren nicht
|
|
22
|
+
weiter (34 Dateien durchsucht, nichts gefunden). Dort steht die Karte
|
|
23
|
+
in einer Datei, die erst `mf-manifest.json` nennt - der Admin laedt
|
|
24
|
+
seine Oberflaeche ueber Module Federation. Also beide Listen
|
|
25
|
+
zusammen, kurze Dateien zuerst.
|
|
26
|
+
|
|
27
|
+
Bricht die Kette - Aufbau geaendert, Datei zu gross -, bleibt die
|
|
28
|
+
Karte leer und ueberall steht der rohe Name. Kein Fehler, nur weniger
|
|
29
|
+
Komfort. Beide Schreibweisen werden verstanden: Werte in Backticks
|
|
30
|
+
(Admin 8) und als JSON-Strings (Admin 7). */
|
|
16
31
|
|
|
17
32
|
import { sprache } from './sprache.js';
|
|
18
33
|
import { holeText } from './basis.js';
|
|
@@ -44,26 +59,76 @@ function ziehe(src) {
|
|
|
44
59
|
return Object.keys(raus).length ? raus : null;
|
|
45
60
|
}
|
|
46
61
|
|
|
62
|
+
/* Welche Dateien der Admin ueberhaupt ausliefert. Zwei Verzeichnisse,
|
|
63
|
+
zusammengelegt und ohne Doppel - siehe Kopf: keines allein reicht
|
|
64
|
+
fuer beide Admin-Fassungen. */
|
|
65
|
+
function adminDateien() {
|
|
66
|
+
var raus = [];
|
|
67
|
+
function dazu(f) {
|
|
68
|
+
/* Fuehrendes `./` weg, sonst zaehlt dieselbe Datei zweimal: das
|
|
69
|
+
Manifest schreibt `assets/lib-….js`, das index.html
|
|
70
|
+
`./assets/lib-….js`. Ohne dieses Abschneiden stuenden 30 Eintraege
|
|
71
|
+
in einer Liste, die nach 25 abgeschnitten wird - und die gesuchte
|
|
72
|
+
Datei faellt genauso heraus wie damals bei der Sortierung nach
|
|
73
|
+
Namenslaenge. */
|
|
74
|
+
f = f.replace(/^\.\//, '');
|
|
75
|
+
if (raus.indexOf(f) === -1) { raus.push(f); }
|
|
76
|
+
}
|
|
77
|
+
/* Gesucht werden PFADE, keine blossen Dateinamen.
|
|
78
|
+
|
|
79
|
+
Beide Verzeichnisse schreiben den Ordner dazu, wo einer gehoert:
|
|
80
|
+
`"sync":["assets/lib-DSjRYjfP.js"]` im Manifest, `assets/…js` im
|
|
81
|
+
index.html. Wer nur den Dateinamen aufsammelt und stur `assets/`
|
|
82
|
+
davorsetzt, erfindet zwei Pfade, die es nicht gibt: das Manifest
|
|
83
|
+
nennt unter `remoteEntry` und `ssrRemoteEntry` je einen Namen mit
|
|
84
|
+
ausdruecklich leerem `"path":""` - die zwei Dateien liegen unter `/`.
|
|
85
|
+
Das gab beim Laden zwei 404 in der Konsole (A1, gefunden 08.09.2026
|
|
86
|
+
produktiv). Geholfen haben sie ohnehin nie: hinter remoteEntry steckt
|
|
87
|
+
der Einstieg von Module Federation, nicht die Sprachkarte.
|
|
88
|
+
|
|
89
|
+
Der Schraegstrich im Muster ist zugleich die Bedingung - ein nackter
|
|
90
|
+
Name kommt so gar nicht erst in die Liste, und ein Admin, der seine
|
|
91
|
+
Buendel eines Tages woanders ablegt, wird trotzdem gefunden. */
|
|
92
|
+
var pfade = /[A-Za-z0-9_\-.]+\/[A-Za-z0-9_\-./]+\.js/g;
|
|
93
|
+
return holeText('/mf-manifest.json')['catch'](function () { return ''; })
|
|
94
|
+
.then(function (mf) {
|
|
95
|
+
(mf.match(pfade) || []).forEach(dazu);
|
|
96
|
+
return holeText('/index.html')['catch'](function () { return ''; });
|
|
97
|
+
})
|
|
98
|
+
.then(function (html) {
|
|
99
|
+
(html.match(pfade) || []).forEach(dazu);
|
|
100
|
+
/* Reihenfolge bleibt, wie die Verzeichnisse sie nennen. Erst
|
|
101
|
+
wurde nach Namenslaenge sortiert - uebernommen vom
|
|
102
|
+
Aufzaehlungskatalog, wo es das Riesenbuendel nach hinten
|
|
103
|
+
schiebt. Hier war es genau falsch: der Name der gesuchten Datei
|
|
104
|
+
ist bei Admin 8 mit 143 Zeichen der laengste ueberhaupt, sie
|
|
105
|
+
rutschte ans Ende und fiel aus der Auswahl. Ungeordnet steht
|
|
106
|
+
sie an fuenfter Stelle von 21, bei Admin 7 an vierter von 19 -
|
|
107
|
+
beide Verzeichnisse nennen das Wichtige zuerst (gemessen
|
|
108
|
+
08.09.2026). Das Groessenlimit unten haelt die dicken Buendel
|
|
109
|
+
ohnehin heraus. */
|
|
110
|
+
return raus.slice(0, 25);
|
|
111
|
+
});
|
|
112
|
+
}
|
|
113
|
+
|
|
47
114
|
export function ladeMusterNamen(nachher) {
|
|
48
115
|
if (geholt) { return; }
|
|
49
116
|
geholt = true;
|
|
50
|
-
|
|
51
|
-
/* Die tab.html nennt mehrere Bausteine (Bootstrap-Haeppchen,
|
|
52
|
-
Vendor, Hauptbundle). Die Karte steckt im Hauptbundle - das
|
|
53
|
-
heisst in beiden bekannten Fassungen index-<hash>.js, also
|
|
54
|
-
dieses zuerst, die uebrigen als Rueckfall der Reihe nach. */
|
|
55
|
-
var dateien = (html.match(/assets\/[A-Za-z0-9_\-.]+\.js/g) || [])
|
|
56
|
-
.filter(function (x, i, a) { return a.indexOf(x) === i; })
|
|
57
|
-
.sort(function (a, b) {
|
|
58
|
-
return (b.indexOf('assets/index-') === 0 ? 1 : 0)
|
|
59
|
-
- (a.indexOf('assets/index-') === 0 ? 1 : 0);
|
|
60
|
-
})
|
|
61
|
-
.slice(0, 8);
|
|
117
|
+
adminDateien().then(function (dateien) {
|
|
62
118
|
if (!dateien.length) { throw new Error('kein Bundle'); }
|
|
63
119
|
return dateien.reduce(function (kette, f) {
|
|
64
120
|
return kette.then(function (fertig) {
|
|
65
121
|
if (fertig) { return true; }
|
|
66
|
-
return holeText('/
|
|
122
|
+
return holeText('/' + f).then(function (src) {
|
|
123
|
+
/* Die Grenze liegt hoeher als beim Aufzaehlungskatalog (3 MB):
|
|
124
|
+
bei Admin 8 steckt die Karte ausgerechnet im grossen
|
|
125
|
+
GUI-Buendel, gemessen 8,4 MB (08.09.2026). Mit der alten
|
|
126
|
+
Grenze wurde genau diese Datei uebersprungen und produktiv
|
|
127
|
+
blieb jeder Beiname roh. Teuer ist es nicht: derselbe Admin
|
|
128
|
+
hat das Buendel gerade selbst geladen, der zweite Griff
|
|
129
|
+
kommt aus dem Zwischenspeicher des Browsers. Alles darueber
|
|
130
|
+
ist kein Buendel mehr, sondern ein Versehen. */
|
|
131
|
+
if (src.length > 12000000) { return false; }
|
|
67
132
|
var karte = ziehe(src);
|
|
68
133
|
if (karte) { musterNamen = karte; return true; }
|
|
69
134
|
return false;
|
package/admin/js/musterwahl.js
CHANGED
|
@@ -112,6 +112,16 @@ export function baueMusterwahl(e, haupt, pflichtFehlt) {
|
|
|
112
112
|
var markeCache = {};
|
|
113
113
|
function bewerte(m) {
|
|
114
114
|
if (markeCache[m] !== undefined) { return markeCache[m]; }
|
|
115
|
+
/* Die angebotene Liste ist gefiltert — der gewaehlte Typ nicht:
|
|
116
|
+
`mBeschriften` ruft `markeFuer(e.want)` direkt, und `e.want` kommt
|
|
117
|
+
ungeprueft aus `v.geraetetyp`. Eine eingelesene Vorlage mit dem
|
|
118
|
+
Musternamen eines neueren type-detector liess `musterVon(m).states`
|
|
119
|
+
werfen, und der Entwurf wurde gar nicht erst gezeichnet — die
|
|
120
|
+
rechte Seite blieb leer (gemessen 09.09.2026). */
|
|
121
|
+
if (!musterVon(m)) {
|
|
122
|
+
markeCache[m] = { mark: tr('pattern.doesNotFit'), passt: false, belegt: 0 };
|
|
123
|
+
return markeCache[m];
|
|
124
|
+
}
|
|
115
125
|
/* Die Vorschau muss vorhersagen, was das Auswaehlen wirklich tut.
|
|
116
126
|
Beim Vorschlag werden Lampe und Steckdose samt Rollen neu gebaut -
|
|
117
127
|
also auch hier gegen den umgebauten Entwurf pruefen. Sonst stuende
|
package/admin/js/objekte.js
CHANGED
|
@@ -81,6 +81,19 @@ export function nachziehenErledigt() {
|
|
|
81
81
|
nachziehBaum = false;
|
|
82
82
|
}
|
|
83
83
|
|
|
84
|
+
/* Nach einem Abbruch sind beim Server alle Abos weg.
|
|
85
|
+
|
|
86
|
+
`subscribeObjects` und `subscribe` gelten nur fuer die Sitzung, die
|
|
87
|
+
sie gestellt hat — der WebSocket-Shim des Admin erneuert von sich aus
|
|
88
|
+
nichts (das Wort `subscribe` kommt in seinem Quelltext kein einziges
|
|
89
|
+
Mal vor). Der `objectChange`-Horcher wird hier ausdruecklich NICHT
|
|
90
|
+
noch einmal angemeldet: der haengt am Socket-Objekt und hat den
|
|
91
|
+
Abbruch ueberlebt. Nur die Anmeldung beim Server fehlt. */
|
|
92
|
+
export function abosErneuern() {
|
|
93
|
+
if (horcht) { socket.emit('subscribeObjects', '*'); }
|
|
94
|
+
(S.abo || []).forEach(function (m) { socket.emit('subscribe', m); });
|
|
95
|
+
}
|
|
96
|
+
|
|
84
97
|
export function horcheAufObjekte() {
|
|
85
98
|
if (horcht) { return; }
|
|
86
99
|
horcht = true;
|
|
@@ -95,11 +108,20 @@ export function horcheAufObjekte() {
|
|
|
95
108
|
var kern = function (o) {
|
|
96
109
|
return o ? JSON.stringify([o.type, o.common, o.native]) : null;
|
|
97
110
|
};
|
|
98
|
-
var vorher = kern(S.objects[id]);
|
|
99
111
|
var nachher = kern(obj);
|
|
100
|
-
if (vorher === nachher) { return; }
|
|
101
112
|
|
|
102
|
-
/* Aufzaehlungen
|
|
113
|
+
/* Aufzaehlungen zuerst — und zwar VOR dem Vergleich mit `S.objects`.
|
|
114
|
+
|
|
115
|
+
Der Vergleich unten fragt `S.objects[id]`, und dort liegen
|
|
116
|
+
Aufzaehlungen nie. Beim Loeschen einer Aufzaehlung sind alter und
|
|
117
|
+
neuer Wert deshalb beide `null`, der Frueheinstieg griff, und
|
|
118
|
+
`delete enums[id]` wurde nie erreicht. Die geloeschte Aufzaehlung
|
|
119
|
+
blieb im Vorrat stehen, und der naechste Alias mit diesem Raum
|
|
120
|
+
kopierte sie samt aller alten Mitglieder zurueck an ihren Platz.
|
|
121
|
+
Gemessen 09.09.2026 am Testsystem: `enum.rooms.WBP7` war am System
|
|
122
|
+
geloescht und waere mit zwei fremden Mitgliedern wieder entstanden.
|
|
123
|
+
|
|
124
|
+
Aufzaehlungen gehen einen eigenen Weg.
|
|
103
125
|
|
|
104
126
|
Sie gehoeren nicht in `objects` - dort stuenden sie im Geraetebaum,
|
|
105
127
|
wo sie nichts zu suchen haben. Nachziehen muss man sie aber, und
|
|
@@ -128,6 +150,10 @@ export function horcheAufObjekte() {
|
|
|
128
150
|
return;
|
|
129
151
|
}
|
|
130
152
|
|
|
153
|
+
/* Alles Uebrige: nur was zaehlt, nicht jeder Schreibvorgang. */
|
|
154
|
+
var vorher = kern(S.objects[id]);
|
|
155
|
+
if (vorher === nachher) { return; }
|
|
156
|
+
|
|
131
157
|
var gabEs = S.objects[id] !== undefined;
|
|
132
158
|
if (obj) { S.objects[id] = obj; } else { delete S.objects[id]; }
|
|
133
159
|
if (gabEs !== (S.objects[id] !== undefined)) { nachziehBaum = true; }
|
package/admin/js/pruefungen.js
CHANGED
|
@@ -68,7 +68,7 @@ export function pruefungDopplung(e) {
|
|
|
68
68
|
export function pruefungSenden(e) {
|
|
69
69
|
var ziele = e.states.filter(function (x) { return x.on && x.srcW; });
|
|
70
70
|
if (!ziele.length) { return { s: 'mut', t: tr('check.publish'), d: tr('check.noWritePath') }; }
|
|
71
|
-
var mqtt = [], stumm = [], fehlt = [];
|
|
71
|
+
var mqtt = [], stumm = [], fehlt = [], unklar = [];
|
|
72
72
|
ziele.forEach(function (x) {
|
|
73
73
|
/* Drei Faelle, nicht zwei: den Punkt gibt es gar nicht, es gibt ihn
|
|
74
74
|
und er ist stumm, oder er sendet. Der erste kam dazu, seit die
|
|
@@ -77,11 +77,19 @@ export function pruefungSenden(e) {
|
|
|
77
77
|
var m = mqttEinstellung(x.srcW);
|
|
78
78
|
if (!m) { return; }
|
|
79
79
|
mqtt.push(x);
|
|
80
|
-
|
|
80
|
+
/* Nur ein ausdrueckliches Nein ist ein Nein. `null` heisst
|
|
81
|
+
unbekannt: Objekte von ioBroker.mqtt und sonoff tragen das Thema
|
|
82
|
+
nur in `native`, ohne `custom`-Block — ueber ihre Sendefaehigkeit
|
|
83
|
+
sagt das nichts. Vorher stand dort fest `false`, und die Pruefung
|
|
84
|
+
meldete rot „kann nicht senden", ohne dass sich etwas daran
|
|
85
|
+
aendern liess (gemessen 09.09.2026). */
|
|
86
|
+
if (m.publish === false) { stumm.push(x.n); }
|
|
87
|
+
else if (m.publish === null) { unklar.push(x.n); }
|
|
81
88
|
});
|
|
82
89
|
if (fehlt.length) { return { s: 'bad', t: tr('check.publish'), d: tr('check.sendPointMissing', fehlt.join(', ')) }; }
|
|
83
90
|
if (!mqtt.length) { return { s: 'mut', t: tr('check.publish'), d: tr('check.notMqtt') }; }
|
|
84
91
|
if (stumm.length) { return { s: 'bad', t: tr('check.publish'), d: tr('check.cannotSend', stumm.join(', ')) }; }
|
|
92
|
+
if (unklar.length) { return { s: 'mut', t: tr('check.publish'), d: tr('check.sendUnknown', unklar.join(', ')) }; }
|
|
85
93
|
return { s: 'ok', t: tr('check.publish'), d: tr('check.canSend', mqtt.length) };
|
|
86
94
|
}
|
|
87
95
|
|
|
@@ -162,7 +162,15 @@ export function tauschPlan(kanal, neuKanal) {
|
|
|
162
162
|
}
|
|
163
163
|
if (!zeile.neuW && zeile.altW) {
|
|
164
164
|
var gw = geraten(zeile.altW, neuKanal);
|
|
165
|
-
|
|
165
|
+
/* Was hier geraten wird, muss ein anderer Punkt sein als die
|
|
166
|
+
Lesequelle. Bei getrennten Quellen heissen Melder und Befehl oft
|
|
167
|
+
gleich - `stat.POWER` und `cmnd.POWER` -, und das Raten greift
|
|
168
|
+
nach dem Namen: aus der Schreibquelle `cmnd.POWER` wurde am
|
|
169
|
+
neuen Geraet `stat.POWER`, also genau der Melder. Der Alias
|
|
170
|
+
haette danach seinen Befehl ins Status-Thema geschickt; das
|
|
171
|
+
Geraet schaltet nicht und die Rueckmeldung ist verfaelscht
|
|
172
|
+
(gemessen 09.09.2026). Lieber kein Treffer als dieser. */
|
|
173
|
+
if (gw && gw !== zeile.neuR) { zeile.neuW = gw; }
|
|
166
174
|
}
|
|
167
175
|
/* Bei einfacher Quelle gilt die Lesequelle fuer beides. */
|
|
168
176
|
if (zeile.einfach && zeile.neuR) { zeile.neuW = zeile.neuR; }
|
|
@@ -191,7 +199,19 @@ export function tauschObjekte(kanal, neuKanal, plan, weg) {
|
|
|
191
199
|
delete neu.ts; delete neu.from; delete neu.user; delete neu.acl; delete neu._id;
|
|
192
200
|
var a = neu.common.alias || {};
|
|
193
201
|
if (z.neuW && z.neuW !== z.neuR) { a.id = { read: z.neuR, write: z.neuW }; }
|
|
194
|
-
else { a.id = z.neuR; }
|
|
202
|
+
else if (z.einfach) { a.id = z.neuR; }
|
|
203
|
+
else {
|
|
204
|
+
/* Getrennte Quellen ohne Schreibtreffer: die Objektform bleibt.
|
|
205
|
+
|
|
206
|
+
Eine blosse Zeichenkette heisst fuer den js-controller „lesen
|
|
207
|
+
und schreiben auf dieselbe Kennung". Aus einem Alias, der den
|
|
208
|
+
Zustand liest und den Befehl woanders hinschickt, wurde damit
|
|
209
|
+
einer, der auf den Melder schreibt - und aus einem bewusst nur
|
|
210
|
+
lesenden Punkt bei `common.write: true` sogar ein schreibender.
|
|
211
|
+
Ohne Treffer bleibt `write` einfach weg: dann schreibt der Alias
|
|
212
|
+
nirgendwohin, und das ist die ehrliche Auskunft. */
|
|
213
|
+
a.id = { read: z.neuR };
|
|
214
|
+
}
|
|
195
215
|
neu.common.alias = a;
|
|
196
216
|
raus.push({ id: z.id, neu: false, obj: neu });
|
|
197
217
|
});
|
|
@@ -461,6 +481,15 @@ function zeichneTausch() {
|
|
|
461
481
|
rechts.appendChild(paarZeile(tr('write.diffNew'),
|
|
462
482
|
z.neuR + (z.neuW && z.neuW !== z.neuR ? ' → ' + z.neuW : ''),
|
|
463
483
|
'sn', gleich ? 'var(--ink-3)' : 'var(--ok)'));
|
|
484
|
+
/* Lesequelle gefunden, Schreibquelle nicht — das gehoert gesagt.
|
|
485
|
+
Sonst steht nur die neue Lesequelle in Gruen da, und dass der
|
|
486
|
+
Punkt danach nirgendwohin schreibt, merkt man erst am Geraet. */
|
|
487
|
+
if (z.altW && z.altW !== z.altR && !z.neuW) {
|
|
488
|
+
var kw = el('div');
|
|
489
|
+
kw.style.color = 'var(--bad)';
|
|
490
|
+
kw.textContent = tr('swap.noWriteMatch');
|
|
491
|
+
rechts.appendChild(kw);
|
|
492
|
+
}
|
|
464
493
|
} else {
|
|
465
494
|
var fehlt = el('div');
|
|
466
495
|
fehlt.style.color = 'var(--bad)';
|