iobroker.alias-workbench 0.9.0 → 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 CHANGED
@@ -567,6 +567,83 @@ imported, and that both language files carry the same keys.
567
567
 
568
568
  ## Changelog
569
569
 
570
+ ### 0.9.7
571
+ This version was measured on the **production system** — 24,786 objects, 140 aliases. None of it shows on the test system with 2,731 objects.
572
+
573
+ * The search box no longer blocks. The tree was rebuilt completely on **every** keystroke: a measured **236 ms per keystroke, 1.9 seconds for eight**. It now draws once, 180 ms after the last keystroke — typing stays responsive. Plus three savings in the tree itself: collapsed nodes are no longer built as DOM at all (previously every subtree was created and immediately thrown away), the question “can this be expanded?” is answered once per row instead of on every sort comparison, and an `Intl.Collator` replaces `localeCompare` with an options object. **The whole tree: 263 → 67 ms.**
574
+ * A redraw no longer costs half a second. `kindZustaende`, `aliasFuer` and their neighbours walked the sorted index **linearly** — 24,786 comparisons for ten hits, some twenty times per redraw, and with live values that is every 700 ms. The list is sorted and the range sits together: a binary search finds it in fifteen steps. Plus a reverse map source → alias built once per index state. **Drawing a source 15.8 → 1.6 ms, an alias with 50 points 51.9 → 14.2 ms.** Proven equal: 3,025 devices and all 727 alias points checked against the old versions, zero differences
575
+ * A read formula can no longer freeze the tab for good. Formulas from `common.alias.read` and from templates run as JavaScript in the admin — on every redraw, per row, unasked. That is no new privilege escalation (whoever may write the formula already has it running in the js-controller), but it is an execution site without limits: `try/catch` does not catch an endless loop. Results are now remembered per formula and raw value — 2000 evaluations in 0.6 ms — and whatever once took longer than 250 ms does not run again; the row says why
576
+ * The object counter in the header no longer gets stuck. Four places set the index directly instead of calling `indexNeu()`, so the short-name map kept deleted ids and `hatPunkt` returned hits on points that no longer existed
577
+ * A full detector run per redraw is gone. Its result travelled through two intermediate steps into a parameter nobody reads. Also removed: `e.andere` and the dead symbols `bestandsWert` and `anzeigeText`
578
+ * The write-source rule lives in one place instead of five. It was spelled out identically three times in `entwurf.js` and twice in `zuordnung.js` — and the comments at two of those sites each document a bug that came from exactly that
579
+ * **Correction to 0.9.6:** adapter and instance roots were meant to become unselectable there — but the check used `_tiefe`, which is the height of the subtree **below** a node, not its position in the tree. On the test system that excluded 131 nodes, exactly one of them rightly: the other 130 were branches such as `…Decklenlicht_RGB.cmnd` and folders such as `iot.0.smart`, whose data points sit directly below. Selectable nodes dropped from 449 to 320. The position is checked now; `mqtt-client` and `mqtt-client.0` stay locked, everything else is back
580
+
581
+ ### 0.9.6
582
+ **Display and flow**
583
+ * “All data points” keeps the output and the manual choice. On a multi-output template at output 2 the rows of output 1 came back while the target still read `…Ausgang_2`; and if automatic matching had found nothing and you picked the template by hand, the click dropped you into raw mode with the choice gone
584
+ * “Differs from the alias” now also shows in alias mode. The marker was handed the target, which is never set there — you are already standing on the alias. So it never appeared exactly where it says the most
585
+ * Switching sources quickly no longer fetches the wrong value. The callback stored the value under whatever source was selected when it arrived — and since only missing values are fetched, it stayed that way
586
+ * The source jump list never appears twice. It hangs off the page body so it survives an automatic redraw; the button is a different one afterwards, and the second click laid a second list over the first
587
+ * A room is suggested even when it is the last segment. With a flat adapter such as `sonoff.0.Wohnzimmer` the search over the source path never ran at all
588
+ * Object names follow the admin language. There were two readers for the same question, one fixed on German before English — so on an English admin, objects, rooms and functions showed up in German. And the name match now looks at every language: “Light” in the system failed to find the function “Licht” and created a duplicate beside it
589
+
590
+ **MQTT and Tasmota**
591
+ * An empty `custom` entry no longer takes anything down. `common.custom` can hold an entry whose value is `null` — ioBroker does not clean it up when an instance disappears. Reaching into it took the whole result view with it
592
+ * `enabled: false` counts as switched off. The field did not appear in the reader at all: `{ enabled: false, publish: true }` counted as publishable although the instance does not serve that point
593
+ * No more false “cannot publish” for ioBroker.mqtt and sonoff. Their objects carry the topic in `native` only; that was reported as a firm “no” instead of “unknown”, the chip was red, and “allow publishing” wrote the object back unchanged — a message that could not be cleared
594
+ * “Answer received” only for a real answer. A `stat.RESULT` left retained from the day before yesterday was enough for the green feedback, even with the device switched off
595
+ * `Vcc` and `Command` are not commands. `{"Command":"Unknown"}` — the reply to an unknown command — stood there as a missing command point, with a create button
596
+ * Command names from foreign JSON no longer reach ids and topics unchecked. A key containing `#` would make a wildcard subscription, one containing `.` an extra level
597
+
598
+ **Templates and devices**
599
+ * The blind gets its STOP. A button press has no state to read, and that case was unknown to template application: without a read path the point dropped out — not a single blind alias ever had a STOP
600
+ * ON_TIME finds its slot. Four templates carried the role `level.timer` while the slot requires `level.timer.off`, so the point ended up in the info device instead of the switching device
601
+ * An explicit formula beats the field. Where a template gives both, the formula was discarded and a new one built from the field
602
+
603
+ **Subscriptions and tree**
604
+ * What is subscribed is what is read — not half the adapter. The patterns came from the display grouping: on a flat MQTT device that produced `mqtt-client.0.*` with over three hundred objects on a single alias. It is now the channels of the actual sources, branches of the same device folded into one pattern, and where even that is too wide, the sources one by one
605
+ * Adapter and instance roots can no longer be selected. `mqtt-client.0` is not a device; selecting it would have subscribed the whole namespace
606
+ * Dark admin themes are recognised. The detection knew four names; `blue` and `colored` fell through and landed on the system setting
607
+ * A late answer to `system.config` still counts. After 2.5 seconds it carries on without one — the answer arriving later was discarded entirely, and without it every expert object was missing from the tree
608
+
609
+ **Housekeeping**
610
+ * The checks touch nothing again. One of them wrote an object from inside the drawing path
611
+ * The delete dialog warns about channels of their own below the target. The hint spoke only of “data points below”
612
+ * Four dead language keys removed — and the test now checks both directions, not just code → JSON
613
+
614
+ ### 0.9.5
615
+ * A broken name hint no longer takes down the whole source view. The text was evaluated as a regular expression unchecked, although neither the template sheet nor the save dialog validates it: after saving `Lampe (Bad`, template matching threw on **every** device selection, no caller caught it, and the right-hand side stayed empty — repairable only through the instance configuration by hand. An unusable expression now counts as “no match” and is flagged in red at the field. The same goes for the nested form `(a+)+`, where the expression engine stalls: a measured 3196 ms for one device name, now none
616
+ * The formula of a fallback source survives updating a template. It was stored under `formel` while every reader looks for `leseformel` — so updating from a device silently dropped it. With `tasmota-steckdose` the consequence was that on devices without `stat.POWER` the boolean alias ended up on the raw JSON text of `tele.STATE` instead of on `POWER`
617
+ * A device type this detector does not know no longer throws. The offered pattern list was filtered, the **selected** type was not — it comes from the template unchecked. An imported template naming a pattern from a newer type-detector made the pattern picker throw, and the draft was never drawn at all
618
+ * Importing now checks more than id and states. `erkennung.erforderlich` as a string instead of a list made template matching throw on **every** device afterwards; a missing `name` threw as soon as the sheet opened. And “import and save” was on screen long before that — you could save a template that then made the tab unusable. `erkennung`, `erforderlich`/`verboten`, `inhalt`, `name`, the name hint and the device type are checked now; a single string that unambiguously means a list is turned into one rather than rejected
619
+ * A hand-typed id no longer replaces someone else's template. Ids were only checked for collisions when switching modes, not after typing — and saving replaces without asking on a matching id, in “new” mode even with version 1. An own template at v7 silently became a different one at v1. The reason is now shown in red at the field and saving is blocked
620
+ * The `%N%` placeholder only hits the output number now. Replacement went purely by trailing digit: on a multi-output device at output 2 the IP row from `tele.INFO2` became `tele.INFO%N%` — and at output 1 that same row reported a foreign output, with “drop the others” throwing it out. Only what actually carries the output becomes a placeholder now: what the template already held as `%N%`, the prefix from `kanalname`, and what appears more than once with the same number
621
+
622
+ ### 0.9.4
623
+ * The tab works again after a dropped connection. It does not load socket.io but the admin's WebSocket shim — and there `connect` fires exactly once per page lifetime, only `reconnect` afterwards. Nobody listened for that, and the shim renews no subscription on its own. After every outage — admin restart, network hiccup, standby — neither object nor state changes arrived, and the dot in the header stayed on “disconnected” although the connection had long been back. `reconnect` is now handled: the display goes back to “connected”, every subscription is renewed and the objects are re-read — anything could have changed while disconnected, and nobody was watching
624
+ * The template mode no longer rebuilds itself every second. Selecting a device with live values and switching to the templates tab rebuilt the whole sheet every 700 ms, including a trial run across all candidates and all templates — the selection deliberately survives the mode switch, but there is no state list on the right any more
625
+ * Where the selection goes, its subscriptions now go too. Unsubscribing only happened when selecting the next node; the four other paths — switching mode, collapsing a branch, leaving expert mode, deleting an alias — unsubscribed from nothing. A subscription then ran on a device no longer shown, and the header counter still claimed “one subscription”
626
+ * Switching nodes while loading no longer leaves the caller hanging. The callback never arrived in that case: “running” stayed on the button forever, and the dry run meant to open after creating missing command points never opened. Twelve seconds of waiting on a SetOption59 query is ample opportunity for that
627
+
628
+ ### 0.9.3
629
+ * An error on the enumerations now stops the move. The first step — creating the new one — always aborted on error, “two rather than none”; the second did not: enumeration errors were collected and the old one was deleted anyway. Afterwards the old alias was gone and the new one sat in no enumeration at all, because the js-controller removes members itself on delete. The assignment was therefore lost outright, not merely orphaned. The same abort now applies one level down, and the button offers “Try again”
630
+ * A partial failure while deleting no longer turns the retry button into a prop. If one delete failed, **all** ids were dropped from the internal store anyway, empty folders and enumerations were cleaned up and the delete target was set to null — which is exactly what `loescheAlias` starts with, so it bailed out immediately. Whatever stayed in the database was out of reach: the workbench considered it deleted. Only what really went is dropped now, the cleanup is skipped, the target stays — and the second click actually finishes the job
631
+ * Orphaned points are deleted on the second attempt too. Their list was cleared after writing **always**, even after a failure; the retry button then wrote the objects but removed nothing, and the message no longer counted them either
632
+ * A load error on your own templates is reported instead of swallowed. If the instance configuration answered with an error, that counted as “no own templates” — the warning chip in the header stayed away, although the timeout path one line above sets it correctly. The consequence was the dangerous part: the workbench considered taken ids free, and the next save would have replaced an existing template with version 1. The chip's tooltip now says so
633
+
634
+ ### 0.9.2
635
+ * Swapping the source no longer writes to the read source: if no new write target is found for a point with separate sources, a plain string was left in `alias.id` — and to the js-controller that means “read and write on the same id”. With Tasmota the command then went to the status topic instead of the command topic: the device does not switch and the feedback is falsified. Worse, the workbench guessed the write source by name and landed on `stat.POWER`, because that too contains “POWER”. A guessed write target now only counts if it is a different point from the read source, the object form is kept, and the row marks the missing match in red
636
+ * Moving recognises two more targets as a collision: one that exists only as a tree node carrying data points — existing points of the same name were overwritten there — and one that sits below the alias itself: `alias.0.X` to `alias.0.X.Sub` took its own parent node along on the final delete
637
+ * A rejected creation of a send point no longer counts as success. The silent path did not even accept the error and entered the object into its own store anyway; after that the guard no longer bit, and “Create now” wrote `alias.id.write` onto an id that never existed in the database — exactly the error the js-controller remembers for good
638
+ * Channel names are cleaned into ids when “Create all” builds one channel per output. “Licht Bar, oben” produced an id with a space and a comma, “Küche.Decke” an extra level including an intermediate folder. And the js-controller does not reject that, it cleans silently — so the object would have appeared under a different id than the one shown. The display name is left untouched
639
+ * A channel keeps its template link across an update, even when the template was deleted or renamed. `native` used to be replaced wholesale; template, version and device type only came back when a template happened to match. The role one line below had long been rescued, `native` had not
640
+ * Hardened: the cleanup of separated sources no longer switches off a point that is itself part of the alias. Whether it happened depended on the order of `Object.keys` — it was not reachable through the interface, because none of the bundled templates puts a reporting row before the controlling one, but the consequence would have been a deleted data point
641
+
642
+ ### 0.9.1
643
+ * A load error on the enumerations can no longer overwrite one: until now a failed `getObjectView` set the store to empty and kept quiet about it. After that the workbench considered every enumeration absent, the catalogue kept offering them because the duplicate filter had nothing to check against, and on writing `setObject` replaced the existing object with a fresh one holding a single member — measured, a room lost three of its four members that way and was given the origin mark that makes it deletable later on. The previous state is now kept, no enumeration is touched, and both the field and the dry run say why room and function are not being written. The alias itself is still created, and a hiccup heals itself after three attempts
644
+ * An enumeration deleted from outside is no longer resurrected: the subscription compared against `objects`, where enumerations never live — on deletion old and new value were therefore both empty and the branch that removes it from the store was never reached. The next alias using that room recreated it with all its old members, including the foreign ones
645
+ * “Create all” now gives every channel its room and its function. The partial drafts carried neither, so not a single channel got an entry — the single route set it, the bulk route did not. A second object for the same enumeration was also discarded, so even then only the first channel would have made it in. And for a channel other than the one shown the assignment counted as empty: if its alias already sat in a room, the bulk route took it **out**. Every channel now keeps what it has, all others get what is set, and members are merged instead of discarded
646
+
570
647
  ### 0.9.0
571
648
  * The role list is complete again: the detector hands out the expressions of its slots as text WITH slashes (`/^indicator…$/`), not as a regular expression. Take the slashes for part of the expression and you find neither start nor end afterwards — 210 roles instead of 232, and `indicator.lowbat` of all things was missing, the role every Homematic device writes. In the role picker, no slot of the pattern was offered at all any more, only the line "show all roles"
572
649
  * From the same cause, BRIGHTNESS lost its role in six colour-light patterns — the slot could be added over and over and could not be filled by any route. Seven places in the code read a slot expression; they all go through one function now, and a test checks the role list against what the detector actually delivers instead of against hand-written expressions
@@ -11,7 +11,9 @@
11
11
  "IPAddress",
12
12
  "StatusSTS",
13
13
  "Wifi",
14
- "Berry"
14
+ "Berry",
15
+ "Vcc",
16
+ "Command"
15
17
  ],
16
18
  "befehle": {
17
19
  "POWER": {
@@ -132,6 +132,7 @@
132
132
  "enums.room": "Raum",
133
133
  "enums.roomHint": "Ordnet den Alias einem Raum zu. Das steht nicht am Objekt, sondern in der Aufzählung „enum.rooms\" — Alexa, Google und die Oberflächen gruppieren danach.",
134
134
  "enums.suggested": "vorgeschlagen",
135
+ "enums.unknown": "Aufzählungen nicht geladen — Raum und Funktion bleiben unangetastet.",
135
136
  "enums.unused": "leer",
136
137
  "enums.willCreate": "neu anlegen",
137
138
  "folder.aliasMissing": "fehlt",
@@ -165,12 +166,9 @@
165
166
  "help.aliasesAdd": "Zum Hinzufügen von Punkten in einem Alias ist der Weg über „zur Quelle →“ (rechts oben neben dem Alias-Namen) bequemer: Dort greift die Vorlage, und alle Datenpunkte des Geräts stehen mit fertigen Rollen zur Auswahl — hier müsstest du jeden einzeln von Hand eintragen.",
166
167
  "help.sources": "Links ein Gerät oder einen Kanal wählen. Die Werkbank erkennt, worum es sich handelt, und schlägt einen fertigen Alias vor. Gibt es ihn schon, wird er stattdessen aktualisiert.",
167
168
  "help.templates": "Links eine Vorlage wählen. Mitgelieferte lassen sich ansehen und duplizieren, eigene bearbeiten. Der Probelauf zeigt, welche Geräte sie trifft.",
168
- "info.countedSeparately": "für die meisten ohne Folgen",
169
169
  "info.dlgTitle": "Gehört nicht zum {0}-Muster",
170
170
  "info.explain": "funktionieren ganz normal und liegen da, wo du sie angelegt hast. Es entsteht dadurch kein zusätzliches Objekt — der Alias bleibt, wie er ist. Sie gehören nur nicht zu dem, was ioBroker unter einem {0} versteht; dafür passt ihre Rolle nicht. Bedeutung hat das erst, wenn du den Alias an Alexa, Google Home oder eine Gerätesteuerung hängst: die sehen dann ein {0} ohne diese Punkte. In VIS, in Skripten und im Verlauf benutzt du sie wie jeden anderen Datenpunkt.",
171
171
  "info.more": "Was das bedeutet",
172
- "info.notPartOf": "Gehört nicht zum {0}",
173
- "info.sameDatapoints": "{0} — ",
174
172
  "leave.body": "An {0} sind Änderungen offen, die noch nicht geschrieben wurden. Wechselst du jetzt, sind sie weg.",
175
173
  "leave.discard": "Verwerfen und wechseln",
176
174
  "leave.stay": "Hierbleiben",
@@ -208,6 +206,7 @@
208
206
  "move.exists": "{0} gibt es schon. Erst entfernen oder einen anderen Namen wählen.",
209
207
  "move.from": "von",
210
208
  "move.needName": "Ein Name muss stehen bleiben.",
209
+ "move.nested": "Das Ziel liegt unter dem Alias selbst (oder umgekehrt). Beim Verlegen würde der eigene Elternknoten mitgelöscht — bitte einen Ort daneben wählen.",
211
210
  "move.newFolder": "Ordner neu",
212
211
  "move.noFolder": "(kein Ordner — direkt unter alias.0)",
213
212
  "move.noScripts": "In den Skripten kommt die Kennung nicht vor.",
@@ -284,7 +283,6 @@
284
283
  "pattern.manualInstead": "von Hand statt {0}",
285
284
  "pattern.missing": "{0} fehlt",
286
285
  "pattern.noMatch": "kein Muster passt zu „{0}“",
287
- "pattern.noPlaceIn": "kein Platz im {0}-Muster",
288
286
  "pattern.noPlaceLegend": "So hinterlegte Zeilen haben keinen Platz im {0}-Muster",
289
287
  "pattern.noPlaceLong": "kein Platz im {0}-Muster — der Punkt wird trotzdem angelegt und ist voll benutzbar, nur Alexa & Co. zählen ihn separat",
290
288
  "pattern.notDetected": "nicht erkannt",
@@ -335,6 +333,7 @@
335
333
  "swap.mindHint": "Die Formeln des Alias bleiben unverändert — rechnet die neue Quelle anders als die alte, stimmen die Werte danach nicht mehr. Die Formel steht in der Detailzeile des Punkts.",
336
334
  "swap.noDevice": "Kein Gerät gefunden.",
337
335
  "swap.noMatch": "kein passender Punkt gefunden",
336
+ "swap.noWriteMatch": "kein Schreibziel gefunden — dieser Punkt schreibt danach nirgendwohin",
338
337
  "swap.now": "Jetzt tauschen",
339
338
  "swap.removeIt": "diesen Punkt beim Tauschen entfernen",
340
339
  "swap.same": "Das ist dieselbe Quelle wie bisher.",
@@ -424,7 +423,7 @@
424
423
  "tpls.id": "Kennung",
425
424
  "tpls.json": "Das wird gespeichert",
426
425
  "tpls.loadFailed": "eigene Vorlagen nicht geladen",
427
- "tpls.loadFailedHint": "Die Instanzkonfiguration hat nicht rechtzeitig geantwortet. Deine eigenen Vorlagen sind nicht verschwunden — sie fehlen nur in dieser Ansicht. Seite neu laden.",
426
+ "tpls.loadFailedHint": "Die Instanzkonfiguration hat nicht geantwortet oder einen Fehler gemeldet. Deine eigenen Vorlagen sind nicht verschwunden — sie fehlen nur in dieser Ansicht. Solange dieser Hinweis steht, kann ein Speichern eine vorhandene Vorlage überschreiben: erst neu laden.",
428
427
  "tpls.losesTo": "verliert gegen {0}",
429
428
  "tpls.multiple": "Dieses Gerät hat mehrere Ausgänge",
430
429
  "tpls.multipleHelp": "Die {0} in den Pfaden wird durch %N% ersetzt — nur am Ende eines Abschnitts, damit ENERGY.Power heil bleibt.",
@@ -549,6 +548,7 @@
549
548
  "write.dryTitleFailed": "Nicht alles ist durchgegangen",
550
549
  "write.dryTitleUpdate": "Alias aktualisieren — das würde entstehen",
551
550
  "write.duplicateName": "{0}: Name kommt doppelt vor.",
551
+ "write.enumsUnknown": "Raum und Funktion werden nicht geschrieben: die Aufzählungen konnten nicht geladen werden. Der Alias selbst entsteht wie gezeigt. Ohne verlässlichen Stand würde eine vorhandene Aufzählung durch eine frische mit nur diesem einen Mitglied ersetzt.",
552
552
  "write.failed": "{0} Vorgänge gingen schief:",
553
553
  "write.new": "neu",
554
554
  "write.noName": "Ein Punkt hat keinen Namen.",
@@ -575,5 +575,16 @@
575
575
  "write.willKeep": "bleiben stehen: {0}",
576
576
  "write.willRemove": "entfernt: {0}",
577
577
  "write.writeSourceMissing": "{0}: den Sendepunkt {1} gibt es noch nicht",
578
- "write.writing": "schreibe …"
578
+ "write.writing": "schreibe …",
579
+ "tpls.idTaken": "Diese Kennung gehört schon „{0}“ (v{1}). Speichern würde sie ersetzen.",
580
+ "tpls.nameHintBad": "Kein brauchbarer Ausdruck — er zählt als „trifft nicht“.",
581
+ "tv.badName": "Der Name muss ein Text oder ein Sprachobjekt sein.",
582
+ "tv.badRecog": "„erkennung“ muss ein Objekt sein.",
583
+ "tv.badList": "„{0}“ muss eine Liste von Punkten sein.",
584
+ "tv.badContent": "„erkennung.inhalt“ muss ein Objekt sein.",
585
+ "tv.badNameHint": "Der Namenshinweis ist kein brauchbarer Ausdruck.",
586
+ "tv.badType": "Den Gerätetyp „{0}“ kennt dieser Detector nicht.",
587
+ "check.sendUnknown": "Ob {0} senden darf, steht nicht im Objekt — das Thema liegt nur in `native`. Bei ioBroker.mqtt und sonoff ist das normal.",
588
+ "del.channelsBelow": "Achtung: {0} eigenständige Kanäle liegen darunter und werden mitgelöscht.",
589
+ "detail.formulaTooSlow": "Diese Formel hat {0} ms gebraucht und wird nicht mehr ausgeführt. Nach dem Neuladen des Reiters wird sie einmal neu versucht."
579
590
  }
@@ -132,6 +132,7 @@
132
132
  "enums.room": "Room",
133
133
  "enums.roomHint": "Assigns the alias to a room. This is not stored on the object but in the \"enum.rooms\" enumeration — Alexa, Google and the dashboards group by it.",
134
134
  "enums.suggested": "suggested",
135
+ "enums.unknown": "Enumerations not loaded — room and function stay untouched.",
135
136
  "enums.unused": "empty",
136
137
  "enums.willCreate": "create new",
137
138
  "folder.aliasMissing": "missing",
@@ -165,12 +166,9 @@
165
166
  "help.aliasesAdd": "To add points to an alias, the way via “to the source →” (top right, next to the alias name) is easier: the template applies there, and every data point of the device is offered with a ready role — here you would have to enter each one by hand.",
166
167
  "help.sources": "Pick a device or channel on the left. The workbench recognises what it is and proposes a finished alias. If one already exists, it is updated instead.",
167
168
  "help.templates": "Pick a template on the left. Bundled ones can be viewed and duplicated, your own edited. The trial run shows which devices it matches.",
168
- "info.countedSeparately": "no consequences for most setups",
169
169
  "info.dlgTitle": "Not part of the {0} pattern",
170
170
  "info.explain": "work perfectly normally and stay where you created them. Nothing extra is written — the alias stays as it is. They simply are not part of what ioBroker considers a {0}; their role does not fit any of its slots. It only starts to matter once you hand the alias to Alexa, Google Home or a device UI: those will see a {0} without these points. In VIS, in scripts and in history you use them like any other datapoint.",
171
171
  "info.more": "What this means",
172
- "info.notPartOf": "Not part of the {0}",
173
- "info.sameDatapoints": "{0} — ",
174
172
  "leave.body": "There are changes on {0} that have not been written yet. Switching now discards them.",
175
173
  "leave.discard": "Discard and switch",
176
174
  "leave.stay": "Stay here",
@@ -208,6 +206,7 @@
208
206
  "move.exists": "{0} already exists. Remove it first or pick another name.",
209
207
  "move.from": "from",
210
208
  "move.needName": "A name is required.",
209
+ "move.nested": "The target sits below the alias itself (or the other way round). Moving would delete its own parent node — please pick a place beside it.",
211
210
  "move.newFolder": "new folder",
212
211
  "move.noFolder": "(no folder — directly under alias.0)",
213
212
  "move.noScripts": "The ID does not appear in any script.",
@@ -284,7 +283,6 @@
284
283
  "pattern.manualInstead": "by hand instead of {0}",
285
284
  "pattern.missing": "{0} missing",
286
285
  "pattern.noMatch": "no pattern matches \"{0}\"",
287
- "pattern.noPlaceIn": "no slot in the {0} pattern",
288
286
  "pattern.noPlaceLegend": "Rows shaded like this have no slot in the {0} pattern",
289
287
  "pattern.noPlaceLong": "no slot in the {0} pattern — the point is still created and fully usable, only Alexa and friends count it separately",
290
288
  "pattern.notDetected": "not detected",
@@ -335,6 +333,7 @@
335
333
  "swap.mindHint": "The alias formulas stay as they are — if the new source scales differently, the values will be wrong afterwards. The formula sits in the point's detail row.",
336
334
  "swap.noDevice": "No device found.",
337
335
  "swap.noMatch": "no matching point found",
336
+ "swap.noWriteMatch": "no write target found — this point will not write anywhere",
338
337
  "swap.now": "Swap now",
339
338
  "swap.removeIt": "remove this point while swapping",
340
339
  "swap.same": "That is the same source as before.",
@@ -424,7 +423,7 @@
424
423
  "tpls.id": "Id",
425
424
  "tpls.json": "This is what gets stored",
426
425
  "tpls.loadFailed": "own templates not loaded",
427
- "tpls.loadFailedHint": "The instance configuration did not answer in time. Your own templates are not gone — they are only missing from this view. Reload the page.",
426
+ "tpls.loadFailedHint": "The instance configuration did not answer or reported an error. Your own templates are not gone — they are only missing from this view. While this notice stands, saving may overwrite an existing template: reload first.",
428
427
  "tpls.losesTo": "loses to {0}",
429
428
  "tpls.multiple": "This device has several outputs",
430
429
  "tpls.multipleHelp": "The {0} in the paths is replaced by %N% — only at the end of a segment, so ENERGY.Power stays intact.",
@@ -549,6 +548,7 @@
549
548
  "write.dryTitleFailed": "Not everything went through",
550
549
  "write.dryTitleUpdate": "Update alias — this is what would appear",
551
550
  "write.duplicateName": "{0}: name appears twice.",
551
+ "write.enumsUnknown": "Room and function will not be written: the enumerations could not be loaded. The alias itself is created as shown. Without a reliable state an existing enumeration would be replaced by a fresh one holding just this single member.",
552
552
  "write.failed": "{0} operations failed:",
553
553
  "write.new": "new",
554
554
  "write.noName": "One point has no name.",
@@ -575,5 +575,16 @@
575
575
  "write.willKeep": "kept: {0}",
576
576
  "write.willRemove": "removed: {0}",
577
577
  "write.writeSourceMissing": "{0}: the send datapoint {1} does not exist yet",
578
- "write.writing": "writing …"
578
+ "write.writing": "writing …",
579
+ "tpls.idTaken": "This id already belongs to “{0}” (v{1}). Saving would replace it.",
580
+ "tpls.nameHintBad": "Not a usable expression — it counts as “no match”.",
581
+ "tv.badName": "The name must be a string or a language object.",
582
+ "tv.badRecog": "“erkennung” must be an object.",
583
+ "tv.badList": "“{0}” must be a list of points.",
584
+ "tv.badContent": "“erkennung.inhalt” must be an object.",
585
+ "tv.badNameHint": "The name hint is not a usable expression.",
586
+ "tv.badType": "This detector does not know the device type “{0}”.",
587
+ "check.sendUnknown": "Whether {0} may publish is not stated in the object — the topic sits in `native` only. Normal for ioBroker.mqtt and sonoff.",
588
+ "del.channelsBelow": "Careful: {0} channels of their own sit below this and will be deleted too.",
589
+ "detail.formulaTooSlow": "This formula took {0} ms and is no longer executed. Reloading the tab gives it one more try."
579
590
  }
@@ -221,8 +221,18 @@ export function raumVorschlag(e) {
221
221
 
222
222
  /* 3. Aus dem Pfad der Quelle */
223
223
  if (quelle) {
224
+ /* Von hinten, das LETZTE Segment eingeschlossen.
225
+
226
+ Frueher begann die Schleife eins davor — gedacht fuer
227
+ `…Wohnzimmer.Lampe`, wo hinten das Geraet steht. Bei einem flachen
228
+ Adapter wie `sonoff.0.Wohnzimmer` IST das letzte Segment aber der
229
+ Raum, und die Schleife lief kein einziges Mal: kein Vorschlag,
230
+ obwohl der Raum im Namen steht (gemessen 09.09.2026 —
231
+ `sonoff.0.Wohnzimmer.Lampe` ergab `enum.rooms.living_room`,
232
+ `sonoff.0.Wohnzimmer` nichts). Der Weg ueber den Geraetenamen
233
+ faengt es nicht auf: der geht bei Name gleich Segment leer aus. */
224
234
  var qt = quelle.split('.');
225
- for (var j = qt.length - 2; j >= 2; j--) {
235
+ for (var j = qt.length - 1; j >= 2; j--) {
226
236
  var qr = suche(qt[j]);
227
237
  if (qr) { return qr; }
228
238
  }
package/admin/js/baum.js CHANGED
@@ -8,7 +8,7 @@ import { S } from './zustand.js';
8
8
  import { $, el } from './basis.js';
9
9
  import { tr, txt, expertenVorgabe } from './sprache.js';
10
10
  import './enums.js';
11
- import { mitNachfrage } from './entwurf.js';
11
+ import { mitNachfrage, aboLoesen } from './entwurf.js';
12
12
  import { zeichneErgebnis } from './ergebnis.js';
13
13
  import { zeichneVorlagenListe } from './vorlagenblatt.js';
14
14
 
@@ -70,7 +70,7 @@ window.addEventListener('storage', function (e) {
70
70
  /* Steht die Auswahl in einem Zweig, den es gleich nicht mehr gibt, muss
71
71
  sie weg — sonst zeigt die rechte Seite ein Geraet, das links fehlt. */
72
72
  if (S.current && !experte() && nurFuerExperten(S.current, S.objects[S.current])) {
73
- S.current = null;
73
+ S.current = null; aboLoesen();
74
74
  S.entwurf = null;
75
75
  }
76
76
  zeichneBaum();
@@ -94,10 +94,6 @@ export function zusatzName(id) {
94
94
  return (n === String(id).split('.').pop()) ? '' : n;
95
95
  }
96
96
 
97
- /* Wonach die Zeile gelesen und sortiert wird. */
98
- export function anzeigeText(k) {
99
- return zusatzName(k.id) || k.name;
100
- }
101
97
 
102
98
  function baueBaum() {
103
99
  var wurzel = { kinder: {}, id: '', name: '' };
@@ -152,6 +148,33 @@ function zaehleZustaende(k, eltern) {
152
148
  }
153
149
 
154
150
 
151
+ /* Ist dieser Knoten aufgeklappt?
152
+
153
+ Stand frueher mitten im Zeichnen einer Zeile — und damit erst,
154
+ nachdem der ganze Teilbaum schon als DOM entstanden war. Fuer
155
+ „zugeklapptes gar nicht erst zeichnen" muss die Antwort vorher da
156
+ sein, also hier. */
157
+ function klappStand(k, tiefe) {
158
+ if ((($('#q') || {}).value || '').trim()) { return true; }
159
+ var vorgabe = (S.alleKlapp === null) ? (tiefe <= 1) : S.alleKlapp;
160
+ /* Im Aliasbaum gibt es unterhalb der Wurzel nur eigene Ordner - ganz
161
+ zugeklappt bliebe eine einzige Zeile `alias` uebrig, und jeder Weg
162
+ begaenne mit zwei Pflichtklicks. Bis `alias.0` bleibt deshalb offen.
163
+ Das steht hier und nicht im Zuklapp-Knopf: sonst gilt es nur, wenn
164
+ man IM Aliasmodus zuklappt - wer in den Quellen zuklappt und dann
165
+ herueberwechselt, fand den Baum zu weit zu (Ricardo, 25.08.2026).
166
+ Einzeln zuklappen darf man sie weiterhin, `S.aufgeklappt` gewinnt. */
167
+ if (S.baumModus === 'aliase' && (k.id === 'alias' || k.id === 'alias.0')) {
168
+ vorgabe = true;
169
+ }
170
+ return S.aufgeklappt[k.id] !== undefined ? S.aufgeklappt[k.id] : vorgabe;
171
+ }
172
+
173
+ /* Ein Vergleicher fuer den ganzen Baum. `localeCompare` mit
174
+ Optionsobjekt baut bei jedem Aufruf einen neuen — bei 24.800 Elementen
175
+ sind das je Sortierlauf zehntausende. */
176
+ var VERGLEICH = new Intl.Collator(undefined, { numeric: true, sensitivity: 'base' });
177
+
155
178
  export function zeichneBaum() {
156
179
  if (S.baumModus === 'vorlagen') { return zeichneVorlagenListe(); }
157
180
  S.baum = baueBaum();
@@ -232,6 +255,23 @@ export function istWaehlbar(k) {
232
255
  /* Notbehelf nur dort, wo im ganzen Zweig keine Typen gepflegt sind.
233
256
  Wo es getypte Kanäle gibt, sollen die das Gerät sein. */
234
257
  if (k._hatTypen) { return false; }
258
+ /* Aber niemals die Wurzel selbst.
259
+
260
+ `mqtt-client` und `mqtt-client.0` sind kein Geraet, sondern der
261
+ Adapter und seine Instanz. Waehlbar waren sie trotzdem, sobald im
262
+ Zweig keine Typen gepflegt sind — und die Auswahl abonniert
263
+ `<id>.*` und holt die Werte des ganzen Namensraums (am Testsystem
264
+ 322 Objekte, in einer gewachsenen Anlage ein Vielfaches). Aufklappen
265
+ ja, auswaehlen nein.
266
+
267
+ Gemeint ist die STELLE im Baum, nicht `_tiefe`: Das ist die Hoehe
268
+ des Teilbaums UNTER dem Knoten, nicht seine Position darin. In
269
+ 0.9.6 stand hier `k._tiefe <= 1`, und das schloss am Testsystem 131
270
+ Knoten aus — genau einen davon zu Recht. Die anderen 130 waren
271
+ Sparten wie `…Decklenlicht_RGB.cmnd` und Ordner wie `iot.0.smart`,
272
+ also Knoten, deren Datenpunkte direkt darunter liegen (gefunden
273
+ 10.09.2026 beim Nachmessen von B23). */
274
+ if (k.id.split('.').length <= 2) { return false; }
235
275
  /* Sonst: alles waehlbar. Was ein Geraet ist, weiss nur der Mensch —
236
276
  bei Duneweaver ist es der Knoten darueber, bei Tasmota der darunter.
237
277
  Statt zu sperren wird geraten (siehe wohlGeraet) und gewarnt. */
@@ -271,12 +311,17 @@ function renderKinder(knoten, filter, tiefe) {
271
311
  return c._zust && !(c.art === 'state' && !Object.keys(c.kinder).length);
272
312
  });
273
313
  }
314
+ /* Einmal je Reihe, nicht bei jedem Vergleich.
315
+
316
+ `klappbar` laeuft ueber alle Enkel, und ein Sortiervergleich ruft es
317
+ zweimal — bei n Reihen also 2·n·log(n) Enkel-Schleifen statt n.
318
+ Dasselbe gilt fuer `localeCompare` mit Optionsobjekt: Das baut je
319
+ Aufruf einen neuen Vergleicher. Ein `Intl.Collator` tut dasselbe
320
+ einmal. */
321
+ reihen.forEach(function (r) { r.kl = klappbar(r.k) ? 0 : 1; r.txt = String(r.zus || r.k.name); });
274
322
  reihen.sort(function (a, b) {
275
- var ba = klappbar(a.k) ? 0 : 1;
276
- var bb = klappbar(b.k) ? 0 : 1;
277
- if (ba !== bb) { return ba - bb; }
278
- return String(a.zus || a.k.name).localeCompare(String(b.zus || b.k.name),
279
- undefined, { numeric: true, sensitivity: 'base' });
323
+ if (a.kl !== b.kl) { return a.kl - b.kl; }
324
+ return VERGLEICH.compare(a.txt, b.txt);
280
325
  });
281
326
 
282
327
  reihen.forEach(function (reihe) {
@@ -320,8 +365,21 @@ function renderKinder(knoten, filter, tiefe) {
320
365
  ? true
321
366
  : (!leer && (k.id.toLowerCase().indexOf(filter) !== -1 ||
322
367
  (reihe.zus && reihe.zus.toLowerCase().indexOf(filter) !== -1)));
323
- var kinderUl = renderKinder(k, filter, tiefe + 1);
324
- var kinderPassen = kinderUl.childNodes.length > 0;
368
+ /* Zugeklappte Knoten gar nicht erst zeichnen.
369
+
370
+ Vorher entstand IMMER der ganze Teilbaum als DOM — auch der eines
371
+ zugeklappten Knotens, der gleich darauf weggeworfen wurde. Bei
372
+ einer gewachsenen Anlage ist das der ganz ueberwiegende Teil der
373
+ Arbeit.
374
+
375
+ Zwei Ausnahmen, in denen der Teilbaum gebraucht wird: beim Filtern
376
+ (ein Treffer weiter unten macht den Elternknoten sichtbar) und
377
+ wenn der Knoten aufgeklappt ist. Sonst genuegt die Frage, OB es
378
+ zeigbare Kinder gibt — und die beantwortet `klappbar` ohne DOM. */
379
+ var aufJetzt = klappStand(k, tiefe);
380
+ var brauchtKinder = !!filter || aufJetzt;
381
+ var kinderUl = brauchtKinder ? renderKinder(k, filter, tiefe + 1) : null;
382
+ var kinderPassen = kinderUl ? kinderUl.childNodes.length > 0 : klappbar(k);
325
383
  if (!passt && !kinderPassen) { return; }
326
384
 
327
385
  var li = el('li');
@@ -329,20 +387,8 @@ function renderKinder(knoten, filter, tiefe) {
329
387
  var d = el('div', 'node' + (waehlbar ? ' pick' : ' grp') + (leer ? ' leer' : '') +
330
388
  (k.id === S.current ? ' sel' : ''));
331
389
 
332
- var hatKinder = kinderUl.childNodes.length > 0;
333
- var vorgabe = (S.alleKlapp === null) ? (tiefe <= 1) : S.alleKlapp;
334
- /* Im Aliasbaum gibt es unterhalb der Wurzel nur eigene Ordner - ganz
335
- zugeklappt bliebe eine einzige Zeile `alias` uebrig, und jeder Weg
336
- begaenne mit zwei Pflichtklicks. Bis `alias.0` bleibt deshalb offen.
337
- Das steht hier und nicht im Zuklapp-Knopf: sonst gilt es nur, wenn
338
- man IM Aliasmodus zuklappt - wer in den Quellen zuklappt und dann
339
- herueberwechselt, fand den Baum zu weit zu (Ricardo, 25.08.2026).
340
- Einzeln zuklappen darf man sie weiterhin, `S.aufgeklappt` gewinnt. */
341
- if (S.baumModus === 'aliase' && (k.id === 'alias' || k.id === 'alias.0')) {
342
- vorgabe = true;
343
- }
344
- var auf = filter ? true
345
- : (S.aufgeklappt[k.id] !== undefined ? S.aufgeklappt[k.id] : vorgabe);
390
+ var hatKinder = kinderPassen;
391
+ var auf = aufJetzt;
346
392
  var tw = el('span', 'tw', hatKinder ? (auf ? '▾' : '▸') : '');
347
393
  if (hatKinder) {
348
394
  tw.addEventListener('click', function (e) {
@@ -394,13 +440,25 @@ function renderKinder(knoten, filter, tiefe) {
394
440
  });
395
441
  }
396
442
  li.appendChild(d);
397
- if (hatKinder && auf) { li.appendChild(kinderUl); }
443
+ if (hatKinder && auf && kinderUl) { li.appendChild(kinderUl); }
398
444
  ul.appendChild(li);
399
445
  });
400
446
  return ul;
401
447
  }
402
448
 
403
- $('#q').addEventListener('input', zeichneBaum);
449
+ /* Entprellt, nicht bei jedem Anschlag.
450
+
451
+ Der Baum wurde bei JEDEM Tastendruck komplett neu gebaut. Am
452
+ Produktivsystem mit 24.786 Objekten gemessen (10.09.2026): 236 ms je
453
+ Anschlag, acht Anschlaege 1,9 Sekunden — die Eingabe hinkte sichtbar
454
+ hinterher. 180 ms sind kurz genug, dass es nach dem Tippen sofort
455
+ dasteht, und lang genug, dass eine fluessig getippte Folge nur einmal
456
+ zeichnet. */
457
+ var suchTimer = null;
458
+ $('#q').addEventListener('input', function () {
459
+ if (suchTimer) { clearTimeout(suchTimer); }
460
+ suchTimer = setTimeout(function () { suchTimer = null; zeichneBaum(); }, 180);
461
+ });
404
462
 
405
463
  /* Alles auf, alles zu. Die einzeln gemerkten Knoten fallen dabei weg -
406
464
  sonst bliebe ein Zweig zu, den man eben ausdruecklich zugeklappt hat,
@@ -416,7 +474,7 @@ function klappeAlle(auf) {
416
474
  Abschalten des Expertenmodus. Geprueft wird an der gezeichneten
417
475
  Liste, nicht an der Datenlage: sichtbar ist, was im Baum steht. */
418
476
  if (S.current && !auf && !imBaumSichtbar()) {
419
- S.current = null;
477
+ S.current = null; aboLoesen();
420
478
  S.entwurf = null;
421
479
  S.openRow = null;
422
480
  zeichneErgebnis();
@@ -448,10 +506,10 @@ export function setzeModus(m) {
448
506
  Der Vorlagenbaum ist ausgenommen: er hat eine eigene Ansicht, und
449
507
  wer von dort zurueckwechselt, findet seine Auswahl wieder vor. */
450
508
  if (m === 'quellen' && S.current && S.current.indexOf('alias.') === 0) {
451
- S.current = null; S.entwurf = null; S.openRow = null;
509
+ S.current = null; aboLoesen(); S.entwurf = null; S.openRow = null;
452
510
  }
453
511
  if (m === 'aliase' && S.current && S.current.indexOf('alias.') !== 0) {
454
- S.current = null; S.entwurf = null; S.openRow = null;
512
+ S.current = null; aboLoesen(); S.entwurf = null; S.openRow = null;
455
513
  }
456
514
  /* Beim Moduswechsel den Filter fallen lassen und die Geraeteknoepfe
457
515
  zurueckholen. Sonst stand man im Vorlagenbaum vor „nichts gefunden"
@@ -149,7 +149,14 @@ export function detailZeile(e, s, idx) {
149
149
  Eine Zeile kann beides tragen — etwa nach einem Musterwechsel, der
150
150
  die Rolle anpasst: Die neue Rolle weicht dann vom Alias ab, und je
151
151
  nachdem auch von der Vorlage (Ricardo, 06.09.2026). */
152
- var bstListe = bestandsAbweichung(s, e.ziel);
152
+ /* `e.ziel || e.kanal` — wie bei den beiden Nachbarn.
153
+
154
+ Im Aliasmodus ist `e.ziel` nie gesetzt: Man steht ja schon auf dem
155
+ Alias, das Ziel waehlt man erst im Quellenmodus. Die Marke „weicht vom
156
+ Alias ab" erschien dort also NIE — ausgerechnet dort, wo sie am
157
+ meisten sagt (gemessen 09.09.2026: mit `e.ziel` null Abweichungen, mit
158
+ `e.ziel || e.kanal` eine). */
159
+ var bstListe = bestandsAbweichung(s, e.ziel || e.kanal);
153
160
  var bstNach = {};
154
161
  bstListe.forEach(function (x) { bstNach[x.feld] = x; });
155
162
 
@@ -227,8 +234,16 @@ export function detailZeile(e, s, idx) {
227
234
  if (selR.value === '__frei__') { s.frei = true; s.srcR = ''; }
228
235
  else { s.frei = false; s.srcR = selR.value; }
229
236
  if (s.srcR && !S.werte[s.srcR]) {
230
- socket.emit('getState', s.srcR, function (err, st) {
231
- if (!err && st) { S.werte[s.srcR] = st; zeichneErgebnis(); }
237
+ /* Die Kennung festhalten, bevor gefragt wird.
238
+
239
+ Der Rueckruf legte den Wert unter der Quelle ab, die BEIM
240
+ EINTREFFEN eingestellt war. Schaltet man in der Zwischenzeit
241
+ weiter, landet der Wert der alten Quelle unter der neuen — und
242
+ da nur geholt wird, was noch fehlt, bleibt es dabei: die neue
243
+ gilt als bekannt (gemessen 09.09.2026). */
244
+ var geholt = s.srcR;
245
+ socket.emit('getState', geholt, function (err, st) {
246
+ if (!err && st) { S.werte[geholt] = st; zeichneErgebnis(); }
232
247
  });
233
248
  }
234
249
  neu();