iobroker.alias-workbench 0.9.10 → 0.9.12

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
@@ -25,6 +25,18 @@ good is decided by things you **cannot see**:
25
25
  A tool that only links datapoints helps with none of that. This adapter shows
26
26
  all four while you are building, not hours later in the log.
27
27
 
28
+ ### Why an admin tab, not a settings page
29
+
30
+ The workbench is an **admin tab** (`adminTab` in `io-package.json`) and runs
31
+ without an adapter process (`mode: none`). That is deliberate: it is an
32
+ interactive editor — an object tree on the left, the draft with live values on
33
+ the right, a dry run before anything is written, a source swap, a template
34
+ sheet. None of that fits into a jsonConfig page, which is built for settings.
35
+ The instance's actual settings do live in a normal jsonConfig page
36
+ (`adminUI.config: "json"`, four switches, translated into all eleven
37
+ languages). Without a process the adapter uses no memory while nobody has the
38
+ tab open.
39
+
28
40
  ## What it does
29
41
 
30
42
  ![The workbench with a device picked](https://raw.githubusercontent.com/RicardoHipp/ioBroker.alias-workbench/main/docs/01-device.png)
@@ -311,6 +323,18 @@ nothing on purpose. Fifty bare `LEVEL` channels are not a device.
311
323
  When a template is rejected, the picker says why: *"1.LEVEL missing"*, not
312
324
  *"does not fit"*. A reason you cannot act on is not a reason.
313
325
 
326
+ **About the role `value.power.consumption`.** Four templates (`messpunkt`,
327
+ `tasmota-lampe`, `tasmota-mehrfach`, `tasmota-steckdose`) give the row
328
+ `CONSUMPTION` the role `value.power.consumption`, which is struck through in
329
+ the ioBroker [state roles list](https://github.com/ioBroker/ioBroker.docs/blob/master/docs/en/dev/stateroles.md).
330
+ It stays on purpose: `@iobroker/type-detector` 6.0.x recognises `CONSUMPTION`
331
+ only with exactly this role (`typePatterns.js`, pattern `consumption`,
332
+ `defaultRole: 'value.power.consumption'`). With `value.energy.consumed` the
333
+ point would drop out of the detected device, and vis, Alexa and the device
334
+ manager would no longer show any consumption. Each of the four rows carries
335
+ this reason in a field `_rolle`. The role will change once the type-detector
336
+ does.
337
+
314
338
  ### Role knowledge beside the rows
315
339
 
316
340
  A template can carry `weiterePunkte`: roles for datapoints that get no row of
@@ -541,12 +565,16 @@ served as they are; editing one and reloading the tab is the whole cycle. On a
541
565
  running installation the admin caches adapter files, so `iobroker upload
542
566
  alias-workbench` after copying is what makes the browser see the change.
543
567
 
544
- Translations live in `admin/i18n/`. A new language is a new file there and an
545
- entry in `SPRACHEN` in `admin/js/sprache.js` — nothing else. Watch for strings
568
+ There are two dictionaries. `admin/i18n/` belongs to the **settings page**
569
+ (jsonConfig with `"i18n": true`, all eleven languages, loaded by the admin
570
+ itself). `admin/sprachen/` belongs to the **tab** (German and English, loaded by
571
+ `admin/js/sprache.js`; any other language falls back to English). A new tab
572
+ language is a new file in `admin/sprachen/` and an entry in `SPRACHEN` in
573
+ `admin/js/sprache.js` — nothing else. Watch for strings
546
574
  written straight into `admin/tab.html`: they are replaced at runtime, and one
547
- that nobody wired up stays German forever without anyone noticing.
575
+ that nobody wired up stays English in every language without anyone noticing.
548
576
 
549
- The tab is **29 ES modules** under `admin/js/`, loaded by the browser as
577
+ The tab is **30 ES modules** under `admin/js/`, loaded by the browser as
550
578
  modules — there is no build step for them, and styling lives in one file,
551
579
  `admin/css/werkbank.css`. Because a module boundary can be crossed wrongly in
552
580
  ways the browser only notices while running, `npm test` checks before every
@@ -564,6 +592,40 @@ imported, and that both language files carry the same keys.
564
592
 
565
593
  ## Changelog
566
594
 
595
+ ### 0.9.12 (2026-09-18)
596
+ * The small **type badges in the tree** (DEV, CHA, FOL …) now all have the same width. A node without an object of its own shows just a dot there, and its badge was narrower — its name fell out of line.
597
+ * **“Move …” did nothing on some aliases.** Creating alias points by hand in the admin often yields an alias without its own channel object — the node shows in the tree but is not an object itself. The workbench displayed the “Move …” button there, but clicking it opened no dialog and said nothing. Such an alias is now moved exactly like one with a channel; a missing channel is not created along the way.
598
+ * **No German left in the English interface.** Three places were hard-wired German: the type badge in the detail row (“Zahl”), the “Datenpunkt” field next to “writes to” and the screen-reader label of the tick boxes (“SET anlegen”). The fixed default texts in `tab.html` are now English — before, 44 German texts were only translated at runtime. The two browser-console messages are English now as well.
599
+ * The tab's **menu entry** now has the adapter's name in all eleven admin languages. Before, it existed in German and English only, and every other language showed “Alias Workbench”.
600
+ * **Why `value.power.consumption`** — four shipped templates give consumption this role, which the ioBroker roles list marks as deprecated. That is deliberate: the type-detector 6.0.x recognises consumption only with exactly this role. The reason is now written into every affected template (field `_rolle`) and into the README. The README also explains why the workbench is an admin tab rather than a settings page.
601
+ * **The object count in the header grew the longer the tab stayed open.** Only devices, channels, folders and states are meant to be counted — and only those are loaded. When following changes, however, the workbench took in every changed object type: after switching the system language, `system.config` sat in its store and the count read 3146 instead of 3145, likewise after every saved instance setting or adapter update. Following changes now applies the same rule as loading.
602
+ * **Values from another node did not update at a source.** When you select a source that already has an alias, the list also shows that alias’s rows — and they often read from somewhere else. The electricity meter shows three rows “from Solar.Netz”: import, smoothed import and house consumption. Their value was fetched once on selection and then froze; it stayed at 428 W for hours while the system reported 22 W. Only the selected node was subscribed; the rows’ sources were only added in “Edit alias” mode. The workbench now subscribes to everything it displays — including a source you point a row to by hand in the detail view. The header accordingly shows two subscriptions instead of one on the electricity meter.
603
+ * The legend above the list claimed **“The light pattern does not apply right now: SET requires boolean, number is set. That is why no row has a place.”** — on a device whose SET row carries `switch.light`/`boolean` and is perfectly fine. The culprit was `0_AES_KEY`: hm-rpc gives it the role `state` and the type `number`, and `state` is allowed on the light pattern’s SET place. The check asks per row “your role would fit a place, your type does not” — it never looks at who ended up holding the place, and the message then printed the PLACE instead of the row it was about. The sentence was wrong at the end too: five rows did have a place. The type reason now only applies when the named place really did stay empty; otherwise the ordinary legend is back, and every placeless row is tinted instead of just one. The case the message was built for — SET itself in the wrong type, place stays empty — is unchanged.
604
+ * Rows carried **“differs from the template”** where no template was in force at all — and flagged the better value while doing so. What was really compared was the raw draft, that is the bare role of the source point: on a switch actuator, after “— no template —”, `SET` showed the badge and below it “The template wants: **switch**”, while the alias holds `switch.light`, which even fits the `light` pattern. Same on `UNREACH`, where `indicator.maintenance.unreach` counted as the deviation and `indicator.unreach` as the target. And it was not only the dismissed case: on a device for which a template is **never** found, the same badge appeared. The rule now: with no template in force — dismissed or never found — no template value is recorded, so the badge, the detail line, the `←` button and the **template** filter button all drop out. Rows keep what the alias holds; that is the stock rule and has nothing to do with templates. With a template everything stays as it was — a real deviation is still shown.
605
+ * With **“— no template —”** the **output** picker stayed on screen if you came from the multi-output template — coming via any other template it was gone. Two routes, two answers on the same device. Worse: using the leftover picker brought the dismissed template **silently back** (measured: after switching to `Licht_Esstisch` the field read “HomeMatic switch actuator with several outputs” again). An output does not belong to the device but to the template’s `mehrfach` block — without it there is no placeholder and so nothing to choose; of 17 bundled templates exactly two carry such a block. The raw draft no longer inherits it, and the channels go through “one sub-folder per channel” instead. **And everything is ticked now.** The raw draft used to tick whatever had a place in the detected pattern; on two **identical** channels the detector picked one of them — the second — so `Licht_Esstisch` stood ticked and `Licht_Bar` empty. Which one it picks is chance, and guessing is not the job of a draft without a template: dropping the template means picking the points yourself.
606
+ * The expander in the template box read **“Why this template +”** and always wanted the right-hand edge. With a long name and two badges — “HomeMatic roller shutter actuator”, chosen by hand, forced — it no longer found room there and took **a line of its own**; the box grew without anything having been added. It is now a **question mark**, with the sentence in the tooltip. What was left out stays visible as a short badge (`4 WITHOUT SOURCE`) — nobody should have to go looking for the fact that points are missing — and the sentence behind it now reads “4 left out because the source is missing” instead of “Left out because the source is missing: 4”. The header no longer wraps at all: room is made by the template name, which is shortened and shown in full in its tooltip; the id stays put, because `hm-rollladen…` is no longer an id. Expanded, the question mark is filled.
607
+ * **A push-button is not simply any write-only point.** The workbench left the read source empty on every row whose source carried `read: false`. But HomeMatic uses that flag for two different things, counted across every device in the house: for a button press (`PRESS_SHORT` 100 points, `RAMP_STOP` 3, `STOP` 1, `OLD_LEVEL` 3 — all with `native.TYPE: ACTION`) **and** for a setting that stays put (`ON_TIME` 20 points, `RAMP_TIME` 3 — `TYPE: FLOAT`). That is why the living-room dimmer showed “never changes” on `ON_TIME` with an empty “reads from” field — on a value in seconds that one does set at the source now and then. Measured: an alias put on `1.ON_TIME`, the source set to 30 directly, and the alias showed 30. So `read: false` does not block reading. The read source is now left empty only where a button press is really meant: `native.TYPE: ACTION`, or a role starting with `button`. `OLD_LEVEL` is the case where the role is not enough — it is called `value.dimmer`.
608
+ * **“— no template —”** is now in the template picker. Once the workbench had detected a template it could only be swapped for another one, never dropped: the first entry of the list was a mere heading with an empty value, and the branch bailed out on an empty value. Anyone wanting to create all points of a device without a template’s defaults had no way to get there. The choice now rebuilds the draft without any template — ticked is whatever has a place in the detected pattern — and target, folder, name, room and function stay as they were.
609
+ * The mark on a row whose source only writes read **“reports nothing”**. That sounded like a fault — as if the device were mute. What is meant is different: the value stays where it was written and never reports back by itself. It now reads **“never changes”**, and the same in the “target does not read” check.
610
+ * **“One sub-folder per channel”** — the button next to “create as one device” serves a different purpose, and nothing said so: it **mirrors the folder structure of the source**. A container for the device, one channel each below it, the states below those. **The ticks now decide what appears**: one sub-folder for every channel that contributes at least one ticked row, holding exactly those rows. Before, the path built a fresh draft per channel and ticked whatever that channel’s pattern offered — things appeared that nobody had chosen (on the test device the maintenance channel got three points instead of the two ticked ones), and channels with nothing chosen were created all the same. Edits made by hand on a row — role, type, unit, formula, caption — now travel with it. **Information channels count too**: they used to be dropped, so the button said “3 of them” while six channels stood in the tree next to it — and clicking `general` on its own has long given a draft (“Information ✓ 7/14 filled”) that could be created. **The maintenance channel gets its own folder** instead of being copied into every output: `UNREACH` exists only in channel 0 of the dimmer, so that is where it belongs; whoever wants it with the light takes “create as one device”. The copying had produced nonsense — 206 raw data points of a lawn mower in each of four drafts, the three meter phases of a Shelly SHEM-3 counted as “shared” instead of as channels of their own. **Virtual channels drop out by themselves**: on a HomeMatic dimmer nothing is ticked in the two `VIRTUAL_DIMMER` channels, so nothing is created there. **States directly on the device** stay in the container, which is then created as a channel instead of a folder. **And the hint is right again**: it computed `targetFolder + channel` while writing uses `targetFolder + device + channel` — on the dishwasher it promised `alias.0.Commands` and created `alias.0.Dishwasher.Commands`, on the living-room dimmer it read `alias.0.1`, `alias.0.2`, `alias.0.3`. It now names the id, the number of ticked points and the detected type. The label says so too: “one sub-folder per channel, N of them …” instead of “one device per channel”
611
+ * On a **multi-output device** the draft adopted the ticks of a foreign alias. The workbench asked "is there already an alias for this device?" and answered with the one that reads **most often** from it. On Ricardo's power strip that is the consumption alias with six points from `tele.SENSOR` — it beat `RF1000` with two points from `stat.POWER1`. Output 1 then showed ELECTRIC_POWER and CONSUMPTION ticked, SET unticked, and the pattern field reported **"✕ SET missing"** on a device with a working SET. The majority rule is right for a single-output device and the wrong question for a multi-output one. The right question is: **does this alias belong to the output currently selected?** That is not guessed from names but asked of the template — which of its paths carry the placeholder? Exactly those tell the outputs apart (`stat.POWER%N%`, `cmnd.POWER%N%`); everything else (`tele.SENSOR`, `tele.LWT`) is shared. This also removes the trap from I31: `tele.INFO2` does carry a two, but no placeholder. For output 1 the bare `cmnd.POWER` counts as well — with Tasmota, POWER without a number is a second name for POWER1, measured at the device: a query to `cmnd/POWER` is answered with `{"POWER1":"ON"}`, and a topic `stat/POWER` does not exist. **The bug only hit the first glance**: coming through the tree brought the wrong stock, switching the output asked nothing at all and the template defaults won — the same output looked different depending on how you got there. All three paths now ask the same question. Single-output devices never enter the new branch by construction; without the new argument `aliasFuer` behaves line for line as before.
612
+ * **The target line now follows the same alias.** On a multi-output device it always showed the output number — `alias.0.Bastelzimmer.Steckdosenleiste.POWER1`, an ID that does not exist — and reported "will be created", while `RF1000` stood right next to it. Clicking would have put a second alias beside the existing ones. The source said: "the workbench cannot know what the individual output is called." That held as long as it could only ask about the device. The target line now points at `…Steckdosenleiste.RF1000` with the badge "already exists", and "update alias" hits it. Where no alias exists for the output yet — the garden valve island — the proposal from the output number stays, and so does "will be created".
613
+ * The **SetOption59** note appeared on every Tasmota device, including where it has no effect. It claimed an alias would show "the old value for minutes after switching" — but on the power strip not a single row reads from `tele/STATE`; they all read from `stat.POWERn`, which the device sends immediately on every command (measured: 1.07 s). "For minutes" also assumed the Tasmota default of 300 seconds; on that strip it is ten (measured: `tele/STATE` every ten seconds). The block now only appears when a row really reads from `tele/…` **and** writes to `cmnd/…`, names those rows, and reads the interval from `TelePeriod`; if it is not in the telegram the text says "on the telemetry cycle" instead of inventing a number. The same applies to the chip in the header. And the check "feedback is immediate" no longer reports "all fine" where there was nothing to check, but **"no row affected — all read from stat/…"** with a grey dot instead of a tick. Card and check ask the same function and can no longer drift apart.
614
+
615
+ ### 0.9.11 (2026-09-16)
616
+ * After saving, the next click in the tree asked **“unsaved changes”** — about an alias that had just been written. The “something unwritten is here” mark was only ever set, never cleared; it went away solely when the draft itself was dropped, i.e. on delete and on move. It now falls after every successful write — and only then: if something went wrong, the change really is not in yet, and the question stays justified. **There is a second effect that had been missing for a long time**: while the mark stood, the workbench deliberately did not rebuild the draft. After a write the old one therefore stayed, instead of rebuilding from the freshly written alias.
617
+ * A second press of **“Suggest free slots”** bent back a row you had set yourself. On the air conditioner: guess nine slots, set the role of the SPEED row to `level.speed` by hand, discard — and the next suggestion put the guessed `level.mode.fan` back. The pattern slot looked free, so the same row was guessed a second time; it then carried **both** marks, “changed” and “guessed”, which exclude each other — what was just guessed cannot have been changed before. Guessing now leaves any row marked “changed” alone. The slot stays free, and the note below says why. Putting a second row next to it was the alternative — the same source point would then hold two slots, and the eighth check would rightly report it.
618
+ * As soon as guesses were pending, the button in the pattern bar renamed itself to **“Discard suggestions”** — and a second one with the same word and the same effect stood in the blue bar below it. Two buttons, one handle. The bar now always says “Suggest free slots”, and only while something is left to guess; once everything is guessed it disappears. Discarding happens where it belongs: in the blue bar, next to “keep all”.
619
+ * Instead of „either every slot is taken, or no remaining datapoint fits" the note now states the case that actually applies — with the **names** of the slots that stay free: “SPEED, SWING … stays free. Every remaining datapoint already serves as a read or write source in a filled row”, or “… No remaining datapoint fits”. If a hand decision holds a slot open, a sentence is appended: “1 rows were decided by hand and are left untouched.” The “every slot taken” case stays silent unless asked — it says “nothing to do”, and would otherwise appear on nearly every device.
620
+ * Ricardo’s air conditioner is detected as a **thermostat** and fills 3 of 24 slots. **airCondition** fails because its only required slot, MODE, does not carry the role — and that settles it, since “does not fit” has the last word. With automatic suggestions it would reach **9 of 29** against 8 of 24. That now stands next to the pattern field: “· airCondition might fit better” — a single line, both numbers in the tooltip. **One** pattern is named — the best one — and only when it really is better; measured in filled slots, not in ratio. Next to it sits a small **→** button that does both in one go: switch to that pattern and press “Suggest free slots” right away — otherwise the note would be an announcement without an action. In the pattern list every affected row carries “with suggestions: fits” behind its reason. It is computed once when the device is selected and after that only on a row change — suggesting reads names, value lists, units and types, no measured values.
621
+ * In the **template sheet** and in the „save as template" dialog the device type showed its **pattern key** instead of its name: a blind read „blinds", a media player „mediaPlayer", a slider „levelSlider" — words that appear nowhere else in the interface. The stored value was right all along (the type, since 11.09.2026); only the label came from the wrong field, and since the Admin does not know those three keys, the translated name fell away with them. It now says the same as the pattern picker next to it: „Jalousien · blind".
622
+ * A **button** can now be created. `STOP` on a blind only writes — a key press has no state to read — and that used to defeat the **whole** alias: the dry run said “STOP: no source chosen”, “create now” stayed greyed out, and not a single blind alias ever got a STOP. The first diagnosis was wrong, and that deserves saying: it was not the write check being too strict. **ioBroker rejects an alias without a read source** — `{ id: { write: … } }` yields “Alias id is invalid: The id is empty!”. The correct shape is the one Ricardo has long been using on his ten hand-built STOPs: `alias.id` as plain text pointing at the command point, plus `common.read: false` on the alias point. That tells vis, Alexa and matter to draw a **button** rather than a toggle stuck at “off” forever. What decides is the **target**, not the role: `read: false` is written by the adapter itself (measured on a real `hm-rpc` STOP). If the target is readable the block stays — then the missing read source really is a defect. The button also survives being read back: all three places that reconstruct an alias now know it, otherwise `read` would quietly flip to `true` on the next update. **The same question grew into a new check**: “target can do what is asked”. The workbench writes to foreign points and reads from foreign points without ever checking whether they can do that. Three ways it silently falls apart — the write formula produces text and the target takes none; the target carries `write: false` (ioBroker stores the value anyway but the adapter passes nothing on: a silent dud); the read source carries `read: false` and never reports anything. All three now appear in the checks panel and as a chip on the ID in the detail view, right where the formula is typed
623
+ * The workbench creates MQTT **send points** itself — a `cmnd` point only comes into existence in ioBroker once something has been published to its topic; before that the alias would have nothing to write to. It took the type from the command knowledge, and `POWER` is listed there as `boolean` — right next to the write formula `val ? "ON" : "OFF"`. The two do not add up: the js-controller converts the formula result back to the type of the **target** on write, the `"ON"` becomes `true` again, and the formula sits in the alias without effect. Switching still works — Tasmota takes `on` as well as `true` — but the point looked different from every point mqtt-client creates, and the formula claimed something it did not do. On the production system it was **one of 196** alias points with an MQTT write target, and it came from us. The rule now is: **where a write formula exists, the point is created as `mixed`** — no exception for `POWER` but a rule that also covers `Fade` and `LedTable`. Without a formula the exact type stays, because the admin derives min, max and unit from `number`. **Plus two places where it becomes visible**: the target point's type now shows as a chip behind its ID in the detail view — right where the formula is typed — in warning colour when the two do not match. And the checks panel has an eighth check, "write formula takes effect", that names it instead of keeping quiet
624
+ * A template now counts as fitting only where its **mandatory rows** actually find their source. Until now detection alone decided, and detection asks a different question than application: `pruefeVorlage` checks whether the named **points** exist, the rows need particular **fields inside them**. A hand-built power-meter template whose mandatory row reads `tele.SENSOR · SML.Total_Summe` thus won on **17** ordinary Tasmota devices and delivered exactly one — and unchecked — row there, where the socket template would have built eleven. The device said "identified as: Tasmota power meter", and the draft was empty. Now it does not even enter the race there and the next best one takes the place; what was skipped is stated in the "why". Optional rows do not count, that is what optional means — measured against the stock, **none** of the fifteen other templates loses any of its 51 wins. **And the try-out in the template sheet**: it now also fetches the values of the points the rows read. Without them it judged fields it could not see, and the same template reported different numbers depending on what had been clicked before. It now shows per device how many rows would really come out, and on "does not fit" **which mandatory row is missing** — turning "22 devices matched, 17 of them won" into the honest "22 matched, 1 won · 21× does not fit". **And the try-out now computes like the creation does**: at first it compared only against the single highest-scoring other template — if that one did not fit the device, the template under test declared itself the winner. "Plain measuring point" thus reported a win on a Tasmota although the socket template sits in between and really fits there. It now walks the match list like `vorschlag` does; measured afterwards, all 150 pairs of template and device agree
625
+ * An alias whose source has vanished from the system is now visible without clicking it. Until now it was only said in the expanded detail and in the checks panel — the row in the list stayed quiet because it asked whether a source was **entered**, not whether it **exists**. The row now carries the red `!` with the note "source does not exist", the header a chip "1 point points nowhere" with the list in its tooltip, and the alias tree a red `!` on the affected channel. This is not cosmetic: the js-controller remembers "source missing" permanently and never corrects it — only deleting and recreating helps. It is computed once when the objects load, not on every redraw; on the production system 531 alias points in 5.2 ms. Two were found straight away: a window contact still pointing at the long-replaced `zigbee` adapter, and an NSPanel point with a missing `0_userdata` source for both reading and writing. A row with **no** source at all keeps its own note — it is unfinished, not broken
626
+ * New: **"Referenced in"** on an existing alias. The box below the change comparison searches on click for whatever else mentions the ID — the **scripts** of the javascript adapter with folder, name, state and line numbers, the **files** of the instance folders (that is, the vis and vis-2 views), every loaded **object** (where the matter bridges keep their members, for instance), the **enumerations** and the entries on the point itself (`SET → influxdb.0`) — and the same again for the **source**, including further aliases on it. That answers the question of what was left behind when moving to the alias. Measured on the production system with 24,706 objects and 66 files: the first look takes 1.3 seconds, every further alias after that 0.17. The web interfaces of the adapters are not searched — 160 of the 177 MB of text in the file tree, identical on every ioBroker, and they carry examples like `alias.0.Floor.Room.MotionSensor`; without that boundary the workbench would find its own source files as users of itself. Deliberately without a verdict: a source that is still mentioned is not a fault — a script that feeds it belongs exactly there (of 376 aliases with a source, 61 still had the source mentioned directly, a good share of them rightly so). **And the delete dialog now searches the scripts too** — that only existed when moving, although deleting is the irreversible one
627
+ * The field picker on a JSON point now copes with every key. Until now the workbench glued the field name onto the access with dots — which only works as long as every part is a valid JavaScript name. `gpu_usages.amd-vaapi.gpu` turned into an arithmetic expression (`?.amd - vaapi?.gpu`), `service.storage./dev/shm.free` and `cpu_usages.1111740.mem` into a syntax error. Measured on a Frigate telegram: of 233 offered fields **27** were usable, the other 206 ended in the red "formula broken" message. `feldFormel` now uses brackets where the key is not a name — `JSON.parse(val)?.gpu_usages?.['amd-vaapi']?.gpu ?? null` — and reads them back again, so the picker stays on the chosen field instead of falling back to "own formula". For ordinary names **not a single character** changes, so the shipped templates are untouched. A dot inside the key itself (`frigate.full_system`) remains unresolvable and still shows "own formula"
628
+
567
629
  ### 0.9.10 (2026-09-13)
568
630
  * 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
631
  * 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
@@ -199,10 +199,15 @@
199
199
  .tree .node.sel .chip { background:rgba(255,255,255,.22); color:var(--accent-ink); border-color:transparent; }
200
200
  .tree .node.sel .nm { color:var(--accent-ink); }
201
201
  .tree .node.sel > span { color:var(--accent-ink) !important; }
202
+ /* Feste Breite: sonst bestimmt der Text die Breite, und ein Knoten ohne
203
+ eigenes Objekt (nur „·“) rueckte seinen Namen aus der Flucht der
204
+ anderen (Ricardo, 18.09.2026). Auch DEV/CHA/FOL/STA sind so gleich
205
+ breit. */
202
206
  .tree .kind {
203
207
  font-size:8px; padding:0 4px; border-radius:4px; background:var(--surface-3);
204
208
  color:var(--ink-3); text-transform:uppercase; letter-spacing:.04em;
205
209
  font-family:var(--sans); font-weight:700; flex:0 0 auto;
210
+ width:26px; box-sizing:border-box; text-align:center;
206
211
  }
207
212
  .tree .idstueck { flex:0 0 auto; }
208
213
  /* Der Name weicht zurueck: gedaempft, und er ist es, der gekuerzt wird,
@@ -377,6 +382,50 @@
377
382
  .aside.w { border-left-color:var(--warn); }
378
383
  .aside b { color:var(--ink); }
379
384
 
385
+ /* Der Kasten „Wird genannt in“.
386
+
387
+ Zuerst mit den Klassen der Zustandsliste gebaut (.slot, .sn, .typ) -
388
+ die sind durchgehend Schreibmaschinenschrift, und der Kasten sah aus
389
+ wie Quelltext statt wie Text. Beschriftung links deshalb wie im
390
+ Detail (.k), Inhalt als gewoehnlicher Satz, und nur Kennungen in
391
+ Schreibmaschine. */
392
+ /* Der Blockkopf muss sich von den Zeilenbeschriftungen darunter
393
+ abheben - stand er in derselben grauen Versalschrift, las sich
394
+ „DER ALIAS“ wie eine weitere Zeile statt wie die Ueberschrift
395
+ darueber (Ricardo, 14.09.2026). Deshalb eigene Leiste mit Grund,
396
+ Akzentfarbe und etwas groesser. */
397
+ .vwkopf {
398
+ display:flex; align-items:baseline; gap:10px; flex-wrap:wrap;
399
+ padding:7px 13px; background:var(--surface-2);
400
+ border-top:1px solid var(--line); border-bottom:1px solid var(--line);
401
+ }
402
+ .vwkopf .t {
403
+ font-family:var(--sans); font-size:10px; text-transform:uppercase;
404
+ letter-spacing:.08em; color:var(--accent); font-weight:700;
405
+ }
406
+ .vwkopf .id { font-family:var(--mono); font-size:12px; color:var(--ink); word-break:break-all; }
407
+ .vwzeile {
408
+ display:grid; grid-template-columns:148px minmax(0,1fr); gap:10px;
409
+ align-items:start; padding:4px 13px; font-size:12px; color:var(--ink);
410
+ }
411
+ .vwzeile .k {
412
+ font-family:var(--sans); font-size:9px; text-transform:uppercase;
413
+ letter-spacing:.06em; color:var(--ink-3); font-weight:700; padding-top:3px;
414
+ }
415
+ .vwzeile .leer { color:var(--ink-3); }
416
+ .vwzeile .id { font-family:var(--mono); font-size:11.5px; word-break:break-all; }
417
+ .vwzeile ul { margin:0; padding-left:15px; }
418
+ .vwzeile li { margin:1px 0; line-height:1.5; }
419
+ .vwzeile li .id { font-weight:600; }
420
+ .vwzeile li .ord { color:var(--ink-3); }
421
+ .vwnote { padding:10px 13px 0; }
422
+ .vwnote:last-child { padding-bottom:12px; }
423
+
424
+ /* Der Alias im Baum, dessen Quelle es nicht mehr gibt. Klein und rot,
425
+ neben Geraetepunkt und Zahl - kein Kasten, keine Toenung: die Zeile
426
+ soll lesbar bleiben. */
427
+ .kaputt { color:var(--bad); font-weight:700; }
428
+
380
429
  .empty { color:var(--ink-3); font-size:13px; padding:36px 8px; text-align:center; }
381
430
  /* Der Erklaertext der leeren Seite: lesbar breit, nicht ueber die
382
431
  ganze Flaeche gezogen, zweiter Absatz gedaempft. */
@@ -460,6 +509,29 @@
460
509
  padding:2px 24px 2px 6px; width:290px; max-width:290px;
461
510
  }
462
511
  .vmeta { display:flex; gap:5px; align-items:center; flex-wrap:wrap; }
512
+ /* „mit Raten: Klimaanlage 9/29, Thermostat 8/24" - der Hinweis neben
513
+ dem Musterfeld. Kein Chip: es ist keine Marke am Geraet, sondern ein
514
+ Weg, den man gehen kann. Deshalb Text in der Farbe der Vermutung,
515
+ nicht in der von Gut oder Schlecht. */
516
+ /* Die Breite ist auf die des Feldes darueber gedeckelt, und der Satz
517
+ darf umbrechen.
518
+
519
+ Mit `nowrap` war er eine 315 px breite Zeile, und weil der Block
520
+ seine Breite aus dem breitesten Kind nimmt, wurde der ganze
521
+ Musterblock breiter. Im Vorlagenkasten passte die Knopfleiste damit
522
+ nicht mehr in dieselbe Zeile, rutschte in die naechste - und deren
523
+ `margin-top:16px` plus der Zeilenabstand von 18 px standen dann als
524
+ 34 px Leere zwischen Hinweis und Knopf (Ricardo, 15.09.2026). Es
525
+ fehlten drei Pixel; jede Wortaenderung haette es wieder kippen
526
+ koennen. Ein Deckel ist verlaesslicher als ein knapper Umbruch. */
527
+ .ratAussicht { font-size:11px; color:var(--accent); max-width:290px;
528
+ display:flex; gap:5px; align-items:center; flex-wrap:wrap; }
529
+ /* Der Knopf traegt die Farbe des Hinweises, nicht die eines
530
+ gewoehnlichen Knopfes - er gehoert zum Satz daneben. Reicht die
531
+ Breite nicht, rutscht er unter den Text statt den Block zu
532
+ verbreitern (siehe den Deckel darueber). */
533
+ .rtgo { color:var(--accent); border-color:var(--accent); flex:none;
534
+ font-size:12px; line-height:1; padding:1px 6px; }
463
535
 
464
536
  /* ---- Geraetekarte ---- */
465
537
 
@@ -715,6 +787,14 @@
715
787
  .tree .geraetepunkt { margin-left:auto; font-size:8px; color:var(--accent); }
716
788
  .tree .node.sel .geraetepunkt { color:var(--accent-ink); }
717
789
  .tree .geraetepunkt + .cnt { margin-left:5px; }
790
+ /* Die Warnung steht in derselben rechten Gruppe wie Geraetepunkt und
791
+ Zahl - und VOR dem Punkt, damit Punkt und Zahl ihr Paar behalten.
792
+ Stand sie dazwischen, verlor `.geraetepunkt + .cnt` seinen Griff und
793
+ die Zahl rutschte allein an den rechten Rand (Ricardo, 14.09.2026). */
794
+ .tree .kaputt { margin-left:auto; font-size:10px; line-height:1; }
795
+ .tree .kaputt + .geraetepunkt { margin-left:6px; }
796
+ .tree .kaputt + .cnt { margin-left:6px; }
797
+ .tree .node.sel .kaputt { color:var(--accent-ink); }
718
798
 
719
799
  .erow .fehltnoch {
720
800
  margin-left:7px; font-style:normal; font-size:10px; font-weight:700;
@@ -765,15 +845,28 @@
765
845
  /* Der Aufklapper steht in der Kopfzeile ganz rechts. `margin-left:auto`
766
846
  schiebt ihn dorthin, ohne dass die Kopfzeile etwas davon wissen muss. */
767
847
  .suggestbox .hd .wkopf {
768
- display:inline-flex; align-items:center; gap:6px; cursor:pointer;
769
- margin-left:auto; padding:2px 7px; border-radius:var(--r-sm);
848
+ display:inline-flex; align-items:center; gap:5px; cursor:pointer;
849
+ margin-left:auto; padding:2px 5px; border-radius:var(--r-sm);
770
850
  font-size:11.5px; font-weight:400; color:var(--ink-3);
771
851
  user-select:none; transition:background .12s, color .12s;
772
- white-space:nowrap;
852
+ white-space:nowrap; flex:0 0 auto;
853
+ }
854
+ /* Statt „Warum diese Vorlage +" ein Fragezeichen mit Tooltip: die
855
+ Kopfzeile ist eng, der Satz ist lang, und gelesen wurde er ohnehin
856
+ nur einmal (Ricardo, 17.09.2026). Aufgeklappt wird das Zeichen
857
+ gefuellt - sonst saehe man dem Kopf nicht an, dass unten etwas steht. */
858
+ .suggestbox .hd .wkopf .fragez {
859
+ display:inline-flex; align-items:center; justify-content:center;
860
+ width:16px; height:16px; flex:0 0 auto; border-radius:50%;
861
+ border:1px solid var(--line-strong); color:var(--ink-2);
862
+ font-size:11px; font-weight:700; line-height:1;
863
+ }
864
+ .suggestbox .hd .wkopf.auf .fragez {
865
+ background:var(--accent); border-color:var(--accent); color:#fff;
773
866
  }
774
- .suggestbox .hd .wkopf .klappz { color:var(--ink-2); font-size:10px; }
775
867
  .suggestbox .hd .wkopf:hover { color:var(--ink); background:var(--surface-2); }
776
- .suggestbox .hd .wkopf:hover .klappz { color:var(--accent); }
868
+ .suggestbox .hd .wkopf:hover .fragez { color:var(--accent); border-color:var(--accent); }
869
+ .suggestbox .hd .wkopf.auf:hover .fragez { color:#fff; }
777
870
  .warum .wleib { margin:0; }
778
871
  .warum .wleib ul { margin:0; padding-left:16px; }
779
872
  .warum .wleib li { margin:1px 0; }
@@ -782,7 +875,25 @@
782
875
  text-transform:uppercase; font-weight:700; color:var(--ink-3);
783
876
  }
784
877
 
785
- .suggestbox .hd { display:flex; align-items:center; flex-wrap:wrap; }
878
+ /* Die Kopfzeile bleibt EINE Zeile. Sie umbrechen zu lassen half nicht:
879
+ der Hinweis rechts hat `margin-left:auto`, fand bei langem Namen und
880
+ zwei Marken keinen Platz mehr und nahm sich eine ganze Zeile fuer
881
+ sich allein. Jetzt weicht stattdessen der Vorlagenname zurueck - er
882
+ steht vollstaendig im Tooltip und im Auswahlfeld darunter. */
883
+ .suggestbox .hd { display:flex; align-items:center; flex-wrap:nowrap; overflow:hidden; }
884
+ .suggestbox .hd > * { flex:0 0 auto; }
885
+ .suggestbox .hd .tname,
886
+ .suggestbox .hd .tid {
887
+ flex:0 1 auto; min-width:0; overflow:hidden;
888
+ text-overflow:ellipsis; white-space:nowrap;
889
+ }
890
+ .suggestbox .hd .tid {
891
+ font-family:var(--mono); font-size:10.5px; font-weight:400; color:var(--ink-3);
892
+ /* Die Kennung weicht gar nicht: sie ist kurz, und abgeschnitten sagt
893
+ sie nichts mehr - `hm-rollladen…` ist keine Kennung. Platz macht der
894
+ Name, der bis auf null zurueckgehen darf. */
895
+ flex-shrink:0;
896
+ }
786
897
 
787
898
  .suggestbox { flex-direction:column; align-items:stretch; gap:9px; }
788
899
  .suggestbox .hd { width:100%; }
@@ -814,6 +925,7 @@
814
925
  /* Gleiche Strichstaerke wie Vorlage und Ausgang daneben - halbfett
815
926
  wirkte wie eine andere Schrift in der Zeile. */
816
927
  .suggestbox .verdictwrap .musterwahl { font-weight:400; width:250px; max-width:250px; }
928
+ .suggestbox .ratAussicht { max-width:250px; }
817
929
  .suggestbox .vmeta { font-size:10.5px; color:var(--ink-3); gap:6px; }
818
930
  .suggestbox .luecke { flex:1 1 20px; }
819
931
  .suggestbox .knoepfe { display:flex; gap:7px; align-items:flex-end; }
@@ -936,6 +1048,10 @@
936
1048
  }
937
1049
  .listbar .verdictwrap.inbar .musterwahl { font-size:12px; width:230px; max-width:230px; }
938
1050
  .listbar .verdictwrap.inbar .vmeta { margin:0; }
1051
+ /* In der Leiste steht der Hinweis NEBEN dem Feld (Reihe statt Spalte)
1052
+ und hat die ganze Breite der Leiste zur Verfuegung - dort ist eine
1053
+ Zeile richtig; reicht sie nicht, bricht die Leiste selbst um. */
1054
+ .listbar .verdictwrap.inbar .ratAussicht { max-width:none; white-space:nowrap; flex-wrap:nowrap; }
939
1055
 
940
1056
  .feldwrap { position:relative; display:block; min-width:0; }
941
1057
  /* Das Musterfeld ist ein Eingabefeld, sieht aber aus wie das Auswahlfeld
package/admin/js/baum.js CHANGED
@@ -412,6 +412,14 @@ function renderKinder(knoten, filter, tiefe) {
412
412
  ueberschreibt ihn die Zeile darueber wieder. */
413
413
  if (leer) { d.title = (d.title ? d.title + ' · ' : '') + tr('tree.emptyNode'); }
414
414
  if (waehlbar) {
415
+ /* Ein Alias, dessen Quelle es nicht mehr gibt, faellt sonst nur
416
+ auf, wenn man ihn anklickt und die Zeile aufklappt. Die Menge
417
+ steht fertig in S.aliasKaputt - hier wird nur nachgeschlagen. */
418
+ if (S.aliasKaputt[k.id]) {
419
+ var kp = el('span', 'kaputt', '!');
420
+ kp.title = tr('tree.sourceGone', S.aliasKaputt[k.id]);
421
+ d.appendChild(kp);
422
+ }
415
423
  if (wohlGeraet(k)) {
416
424
  var pk = el('span', 'geraetepunkt', '●');
417
425
  pk.title = tr('tree.looksLikeDevice');
@@ -4,11 +4,11 @@ import { S } from './zustand.js';
4
4
  import { socket } from './verbindung.js';
5
5
  import { el } from './basis.js';
6
6
  import { tr } from './sprache.js';
7
- import { wertVon, jsonFelder, feldAusFormel, feldFormel, feldWert, jsonVon } from './werte.js';
7
+ import { wertVon, jsonFelder, feldAusFormel, feldFormel, feldWert, jsonVon, zielKann } from './werte.js';
8
8
  import { musterVon, erkenneEntwurf, platzFuerRolle, typenFuerRolle } from './erkennung.js';
9
9
  import { musterName } from './musternamen.js';
10
10
  import { rollenFeld } from './rollenwahl.js';
11
- import { opt , quellenAuswahl, vorlagenAbweichung, bestandsAbweichung } from './entwurf.js';
11
+ import { opt , quellenAuswahl, vorlagenAbweichung, bestandsAbweichung, aboErgaenzen } from './entwurf.js';
12
12
  import { zeichneErgebnis, entwurfAngefasst } from './ergebnis.js';
13
13
 
14
14
  import { rateBehalten } from './vorschlagen.js';
@@ -26,7 +26,13 @@ export function detailZeile(e, s, idx) {
26
26
 
27
27
  /* Wer die Zeile anfasst, hat entschieden - die Vermutung ist damit
28
28
  keine mehr und faellt beim Zuruecknehmen nicht mehr weg. */
29
- function neu() { s.geaendert = true; rateBehalten(s); entwurfAngefasst(); zeichneErgebnis(); }
29
+ /* Eine von Hand umgestellte Quelle wird mit abonniert - sonst stuende
30
+ ihr Wert nach dem ersten Abholen still (J9b). */
31
+ function neu() {
32
+ s.geaendert = true; rateBehalten(s); entwurfAngefasst();
33
+ aboErgaenzen([s.srcR, s.srcW]);
34
+ zeichneErgebnis();
35
+ }
30
36
 
31
37
  /* ---- Die Auswahl der Datenpunkte ---------------------------------
32
38
 
@@ -71,15 +77,59 @@ export function detailZeile(e, s, idx) {
71
77
  steht die volle Kennung als eigene Zeile unter dem Feld. Dieselbe
72
78
  Bauart wie „Die Vorlage will hier: …": sie darf umbrechen und hat
73
79
  damit keine Breitengrenze. */
74
- function mitVollId(felder, id) {
80
+ function mitVollId(felder, id, zusatz) {
75
81
  var w = el('div', 'feldstapel');
76
82
  w.appendChild(felder);
77
83
  var v = el('div', 'vollid');
78
84
  if (id) { v.textContent = id; v.title = id; }
85
+ /* Beim Schreibziel steht hinter der Kennung sein Typ. Er entscheidet,
86
+ ob die Schreibformel ueberhaupt ankommt - der js-controller rechnet
87
+ den Formelwert auf den Typ des ZIELS zurueck. Man tippt die Formel
88
+ also an einer Stelle, an der man bisher nicht sehen konnte, ob sie
89
+ etwas bewirkt (Ricardo, 15.09.2026). */
90
+ if (zusatz) { v.appendChild(zusatz); }
79
91
  w.appendChild(v);
80
92
  return w;
81
93
  }
82
94
 
95
+ /* Was der fremde Punkt kann, als kleine Marke hinter seiner Kennung.
96
+
97
+ Beide Seiten zeigen dasselbe, nur aus verschiedener Richtung: am
98
+ Schreibziel seinen Typ (er entscheidet, was von der Formel ankommt)
99
+ und ob es sich ueberhaupt beschreiben laesst; an der Lesequelle, ob
100
+ sie etwas meldet. `zielKann` in werte.js ist die eine Stelle, die
101
+ darueber urteilt - die Pruefungen-Karte fragt dieselbe Funktion. */
102
+ function zielMarke(s, seite) {
103
+ var id = seite === 'w' ? s.srcW : s.srcR;
104
+ if (!id) { return null; }
105
+ var o = S.objects[id];
106
+ if (!o || !o.common) { return null; }
107
+ var c = o.common;
108
+
109
+ var maengel = zielKann(s).filter(function (x) {
110
+ return seite === 'w' ? x.art !== 'meldetNichts' : x.art === 'meldetNichts';
111
+ });
112
+
113
+ var text, hinweis;
114
+ if (seite === 'w') {
115
+ if (!c.type) { return null; }
116
+ text = c.type;
117
+ var nw = maengel.some(function (x) { return x.art === 'nichtSchreibbar'; });
118
+ var bl = maengel.some(function (x) { return x.art === 'formelBlind'; });
119
+ hinweis = nw ? tr('detail.targetNoWrite')
120
+ : (bl ? tr('detail.targetTypeVoid', c.type) : tr('detail.targetTypeHint', c.type));
121
+ if (nw) { text = c.type + ' \u00b7 ' + tr('detail.notWritable'); }
122
+ } else {
123
+ if (!maengel.length) { return null; }
124
+ text = tr('detail.reportsNothing');
125
+ hinweis = tr('detail.targetNoRead');
126
+ }
127
+
128
+ var m = el('span', 'chip ' + (maengel.length ? 'warn' : 'mut'), text);
129
+ m.style.marginLeft = '7px';
130
+ m.title = hinweis;
131
+ return m;
132
+ }
83
133
  /* ---- Wo die Vorlage etwas anderes will --------------------------
84
134
 
85
135
  Die zugeklappte Zeile sagt nur, DASS etwas abweicht. Welches Feld,
@@ -282,7 +332,7 @@ export function detailZeile(e, s, idx) {
282
332
  lF.appendChild(selF);
283
333
  qf.appendChild(lF);
284
334
  }
285
- zeigeBeide(zeile(tr('detail.readsFrom'), mitVollId(qf, s.srcR)), ['srcR']);
335
+ zeigeBeide(zeile(tr('detail.readsFrom'), mitVollId(qf, s.srcR, zielMarke(s, 'r'))), ['srcR']);
286
336
 
287
337
  if (s.frei) {
288
338
  var iFrei = el('input', 'tx w');
@@ -314,7 +364,7 @@ export function detailZeile(e, s, idx) {
314
364
  neu();
315
365
  });
316
366
  var lW = el('label', 'fld');
317
- lW.appendChild(el('span', null, 'Datenpunkt'));
367
+ lW.appendChild(el('span', null, tr('detail.datapoint')));
318
368
  lW.appendChild(selW);
319
369
  wf.appendChild(lW);
320
370
  /* Der Hinweis gilt nur, wenn die beiden Quellen wirklich
@@ -322,7 +372,7 @@ export function detailZeile(e, s, idx) {
322
372
  if (s.srcW && s.srcW !== s.srcR) {
323
373
  wf.appendChild(el('div', 'sugg', tr('detail.separateHint')));
324
374
  }
325
- zeigeBeide(zeile(tr('detail.writesTo'), mitVollId(wf, s.srcW)), ['srcW']);
375
+ zeigeBeide(zeile(tr('detail.writesTo'), mitVollId(wf, s.srcW, zielMarke(s, 'w'))), ['srcW']);
326
376
 
327
377
  /* --- Leseformel --- */
328
378
  var fb = el('div');
@@ -436,7 +486,10 @@ export function detailZeile(e, s, idx) {
436
486
  iU.value = s.unit || '';
437
487
  iU.placeholder = '—';
438
488
  iU.addEventListener('click', function (ev) { ev.stopPropagation(); });
439
- iU.addEventListener('input', function () { s.unit = iU.value; s.geaendert = true; });
489
+ /* `rateBehalten` gehoert zu jedem `geaendert` dazu - sonst traegt die
490
+ Zeile beim naechsten Zeichnen beide Marken („geaendert" UND
491
+ „vermutet"), und die schliessen sich aus (Ricardo, 15.09.2026). */
492
+ iU.addEventListener('input', function () { s.unit = iU.value; s.geaendert = true; rateBehalten(s); });
440
493
  iU.addEventListener('change', function () { neu(); });
441
494
  var lU = el('label', 'fld');
442
495
  lU.appendChild(el('span', null, tr('detail.unit')));
@@ -461,7 +514,7 @@ export function detailZeile(e, s, idx) {
461
514
  iC.value = s.caption || '';
462
515
  iC.placeholder = s.n;
463
516
  iC.addEventListener('click', function (ev) { ev.stopPropagation(); });
464
- iC.addEventListener('input', function () { s.caption = iC.value; s.geaendert = true; });
517
+ iC.addEventListener('input', function () { s.caption = iC.value; s.geaendert = true; rateBehalten(s); });
465
518
  /* Was das Feld tut, stand nirgends - man tippte und sah keine
466
519
  Wirkung. Der Wert wird der Anzeigename des Punkts (common.name)
467
520
  und erscheint als Zusatz in der Zeile darueber. */
@@ -496,7 +549,7 @@ export function detailZeile(e, s, idx) {
496
549
  var t = typeof a.val;
497
550
  var passt = !s.typ || s.typ === t || s.typ === 'mixed';
498
551
  box.appendChild(el('span', 'chip ' + (passt ? 'ok' : 'warn'),
499
- (passt ? '✓ ' : '≠ ') + (t === 'number' ? 'Zahl' : (t === 'boolean' ? 'boolean' : t))));
552
+ (passt ? '✓ ' : '≠ ') + (t === 'number' ? tr('detail.typeNumber') : t)));
500
553
  if (a.gewandelt) {
501
554
  box.appendChild(document.createTextNode(' '));
502
555
  box.appendChild(el('span', 'chip mut', tr('detail.converted')));