checks / unit (pull_request) Successful in 6m20s
UX-01's three unmet acceptance criteria, closed out. It shipped in 0.7.0 and is LIVE, so all three describe the running install. No behaviour, no schema and no default moves here: config.yaml, translations/ and every module under app/ are byte-identical to6e7b233. ⚠️ AC 3 - "the visible count drops to ~15" - IS WITHDRAWN AS UNSATISFIABLE, and the arithmetic is the reason rather than the effort. options: and schema: both declare all 71 keys, so the set HA's toggle can hide is EMPTY - it hides a key only when it is ?-optional AND absent from options:, and nothing is absent. But the deeper fact is that no manifest could have met it: Supervisor's own UiOptions marks 59 of the 71 REQUIRED, and the frontend's own filter - `schema.filter(e => e.name in opts || e.required)`, transcribed out of the bundle in the release image - shows a required field unconditionally. So 59 is the floor with options: EMPTIED ALTOGETHER. The target was ~4x below anything reachable. ⚠️ AND THE 12 HIDES THAT ARE EXPRESSIBLE WOULD BE BOUGHT WITH THE UPGRADE PATH. A key absent from options: has no default for the Supervisor to deep-merge into an existing box's stored blob. That is exactly the delivery_hold_s mechanism this release rests on: a REQUIRED key a 0.7.0 blob has never stored resolves to its options: default of 185 instead of becoming exit 78. Hiding a key is precisely the edit that removes that. It would also promote the 27 unpinned in-code literal fallbacks the new check ledgers into the ACTUAL source of the defaults on a live install - the CFG-03 defect the same card's AC 4 exists to police, made load-bearing by its AC 3. ⚠️ THE COUNT WAS PRINTED AND NEVER GRADED, WHICH IS HOW THIS PASSED REVIEW. supervisor_shape_probe.py section F has printed "the Configuration tab renders 71 fields" since the day it was written and exited 0 through UX-01's whole review while displaying the number that contradicted the criterion it was evidence for. Three assertions replace the bare print, every number computed from the manifest and from Supervisor's own `required` flag rather than typed, including an ISOLATED COUNTERFACTUAL that empties options: and nothing else. MEASURED RED: delete one key (price_feed_url) from options: and two of them fail - `Hideable: ['price_feed_url']`, `70 of 71 fields render` - exit 1. --- AC 4: the AST check, built -------------------------------------------- AC 4's first clause is VACUOUS over an empty set (see above) and its second - "the table changed goes red" - already works six times over. The third, the one with the content, was never written: the only literal scan in the repository is test_validate.py's regex over eight maintenance_* keys, which is a grep and predates the card. test_defaults_pin.py walks ast.Call nodes over app/*.py. It: - DISCOVERS the six *_OPTION_DEFAULTS mirrors from the tree rather than naming them. Every existing pin names its own table by hand, so a SEVENTH table added tomorrow is pinned by nobody and nothing says so; - grades all 30 mirrored keys against config.yaml, and requires that no module carries a literal fallback for any of them; - FREEZES the CFG-03 backlog at exactly today's 27 (module, key) pairs, by pair and not by count - a count goes green again the moment one site is deleted and another added. Line numbers are deliberately out of the key: a ledger that reddens on an unrelated reformat is one people re-bless without reading. It can only shrink; - grades every remaining literal AGAINST config.yaml, which is the half that catches CFG-04's actual failure - two copies of one number, one moves. ⭐ ONE PREVIOUSLY UNREPORTED DIVERGENCE, and it is what that last check is for: main.py:290 answers a missing target_grid_w with 0 where config.yaml ships -10. It cannot reach an operator - the deep-merge guarantees the key is present - and the only paths that hand over a partial dict are sim/inject.py and this repo's fixtures. Recorded as the single permitted divergence with its reason rather than corrected; changing it alters the control loop's target and belongs to a card that grades the loop. A second divergence goes red. ⭐ THE RED DIRECTION WAS RUN, TWICE OVER. Part 5 feeds the scanner a synthetic module containing a deliberate violation of each rule and REQUIRES it to be caught, in-process, through the same function the graded parts call - so a scanner that goes blind stops working there too - and the site total is graded against a floor. And a literal was planted in the REAL tree: main.py:290 float(opts.get("maintenance_charge_w", 2000)) -> FAIL no literal fallback survives for any of the 30 keys a mirror already answers for exit 1 main.py:290 opts.get("p1_host", "x"), opts.get("slew_w", 999) -> FAIL no NEW module/key pair has started carrying a literal fallback [('main.py', 'p1_host')] FAIL every remaining literal still AGREES with config.yaml main.py slew_w: code 999 vs config.yaml 0 exit 1 Both plants removed. --- AC 5: CFG-04 is closed, and not by this card's predecessor -------------fef6ef2touched config.yaml, translations/, test_translations.py and the probe - no defaults code at all - and on 0.7.0 the max_w 2000 -> 2500 triple mutation survives 21 files, 0 RED. CONFIRMED CLOSED on release/1.0 by re-running the same triple mutation on a throwaway copy of6e7b233: 26 files, RED at test_selfconfig.py, exit 1 - 1 FAILED: ⚠️ CLEARING max_w ON THE LIVE SHAPE IS REFUSED - rule 10 catches the rail collapsing to the 2000 default under an explicit 400 W slew git log -L 310,325:test_selfconfig.py attributes that check tobcee235, "UX-02: pin both halves of the max_w fix, because neither was graded". The AC is met BY UX-02 and ships with the pending 0.9.0 deploy. Nothing was re-done. --- the 8 per-entry translations: they stay -------------------------------- Measured negative and not a defect in them: UiOptions gives inverters type schema WITH multiple, the frontend sends that case to ha-selector-object, which labels fields `fields[name].label || name`, and the converter sets neither label nor translation_key. The Supervisor tab CANNOT be made to render them. They stay because UX-02's ingress panel is the surface that can, because they cost nothing, and because deleting them removes the only thing that would catch a per-entry key added without a translation - which is how SAFETY-32's REQUIRED bms_soc_floor would have joined the roster unlabelled. A written decision, not a change; translations/ is untouched. tools/supervisor_selfapi_probe.py stays red on its hardcoded len(_sub) == 7. That is TOOL-04, already filed, and it is not re-filed or fixed here. --- evidence --------------------------------------------------------------- positive control test_defaults_pin.py's mirror floor moved 6 -> 7 inside the harness container: "### RED: test_defaults_pin.py", exit 1 suite 27 files, ALL GREEN, exit 0 (goodwe-addon-checks:1, the checkout mounted read-only) shape probe all cases passed, exit 0, on ghcr.io/home-assistant/amd64-hassio-supervisor:2026.08.0 Nothing here touched hardware, the live Home Assistant at 192.168.2.158, or sim/. Both probe runs were throwaway containers over a read-only mount. DECISIONS/UX-04-the-configuration-tab-and-the-defaults-pin.md carries all four decisions, the frontend transcription, and the two counterfactual transcripts. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01BwRLw7eKYj2byVdwB6kbjU
380 lines
18 KiB
Markdown
380 lines
18 KiB
Markdown
# UX-04 — what the Configuration tab renders, and the pin that was never built
|
|
|
|
**Status: DECIDED 2026-09-07.** Three decisions and one confirmation, in the
|
|
order [[UX-01]]'s unmet acceptance criteria are numbered:
|
|
|
|
1. **UX-01 AC 3 (hide the optional fields behind HA's toggle) is WITHDRAWN as
|
|
unsatisfiable**, not deferred. The target was arithmetically unreachable, and
|
|
the part of it that *was* reachable would be bought with the upgrade path.
|
|
The measurement is now **graded** in `tools/supervisor_shape_probe.py` §F
|
|
rather than printed, so the withdrawal cannot be quietly reversed.
|
|
2. **UX-01 AC 4's third clause is now BUILT**: `test_defaults_pin.py`, an AST
|
|
check, with its red direction executed on every run.
|
|
3. **UX-01 AC 5 / the CFG-04 double-literal hole is CONFIRMED CLOSED on
|
|
`release/1.0`, by [[UX-02]]'s `bcee235`, not by UX-01's `fef6ef2`.** The
|
|
attribution is corrected here; no work was re-done.
|
|
4. **The 8 per-entry `inverters:` translations STAY.** They do not reach the
|
|
Supervisor tab and cannot be made to; [[UX-02]]'s ingress panel is where they
|
|
become visible.
|
|
|
|
Filed with UX-04. Card `6a9ec94617e9c48ed74ca5dd`. Every number below was
|
|
measured on `release/1.0` at `6e7b233` on 2026-09-07, against
|
|
`ghcr.io/home-assistant/amd64-hassio-supervisor:2026.08.0` — the version
|
|
`ha info` reports on the live install — and against the 26-file check suite in
|
|
`goodwe-addon-checks:1`. Nothing here contacted the live Supervisor at
|
|
192.168.2.158, posted options, or restarted anything.
|
|
|
|
---
|
|
|
|
## 1. Hiding: withdrawn, and why that is the *safe* answer
|
|
|
|
### 1.1 What AC 3 claimed
|
|
|
|
> "The visible count drops to ~15, with the rest behind HA's *show unused
|
|
> optional configuration options* toggle."
|
|
|
|
### 1.2 What the tab actually renders
|
|
|
|
`config.yaml` declares **71 keys under `options:` and the same 71 under
|
|
`schema:`**. The set that HA's toggle is *able* to hide — schema-only keys — is
|
|
therefore **empty**:
|
|
|
|
```
|
|
hideable = set(schema) - set(options) -> [] (0 of 71)
|
|
```
|
|
|
|
On the deployed 0.7.0 tree the same measurement is 70/70. Nothing was hidden
|
|
before UX-01, nothing is hidden after it, and no line of `fef6ef2` moved a key
|
|
either way. AC 3 is not partially met; it is unstarted.
|
|
|
|
### 1.3 The rule that decides it, replayed rather than paraphrased
|
|
|
|
From the frontend bundle shipped **in this image**,
|
|
`api/panel/frontend_latest/5680.*.js` (frontend 20250925.1):
|
|
|
|
```js
|
|
_filteredSchema = (opts, schema) => schema.filter(e => e.name in opts || e.required)
|
|
```
|
|
|
|
`opts` is the add-on's options **as Supervisor serves them** — the operator's
|
|
stored blob after `options:` has been deep-merged into it. So a field is hidden
|
|
only when it is **both** absent from `options:` **and** not `required`. The
|
|
probe now runs that filter on this manifest rather than quoting it.
|
|
|
|
### 1.4 Reason one: the target was never reachable
|
|
|
|
Supervisor's own `UiOptions` marks `required` on **59** of the 71 fields; 12 are
|
|
optional. The filter above shows a required field unconditionally. So the
|
|
**floor** — what the tab renders with `options:` emptied *altogether*, which is
|
|
the most hiding that is even expressible — is 59 fields.
|
|
|
|
| `options:` block | fields rendered |
|
|
|---|---|
|
|
| as shipped (71 keys) | **71** |
|
|
| emptied entirely (0 keys) | **59** |
|
|
| AC 3's target | ~15 |
|
|
|
|
AC 3 asked for a number roughly four times below the floor. No arrangement of
|
|
`options:` reaches it. The criterion could not have been met by any
|
|
implementation; it was mis-specified, and the honest disposal of a
|
|
mis-specified criterion is withdrawal, not a partial gesture toward it.
|
|
|
|
### 1.5 Reason two: the 12 hides that *are* expressible cost the deep-merge
|
|
|
|
Moving a key out of `options:` is not free. `App.options` is
|
|
`deepmerge(config.yaml defaults, the stored user options)`, so **a key absent
|
|
from `options:` has no default for the Supervisor to merge**.
|
|
|
|
That is exactly the mechanism the 0.9.0 upgrade rests on. From `CHANGELOG.md`,
|
|
the pre-deploy check run against this release on 2026-09-07:
|
|
|
|
> The reference install's stored blob has **70 keys, not 71**.
|
|
> `delivery_hold_s` arrived with SAFETY-42 in 0.8.0, so an install running
|
|
> 0.7.0 has never stored it — and it is REQUIRED in the schema, no trailing
|
|
> `?`, which is the shape that becomes **exit 78**. It resolves to its
|
|
> `options:` default of **185** because the Supervisor deep-merges.
|
|
|
|
Hiding a key is done by taking it *out* of `options:` and marking it `?`. That
|
|
is, precisely, the edit that removes a key's merge default — the thing standing
|
|
between a 0.7.0 box and exit 78 on upgrade. It would be traded for at most 12
|
|
fewer rows on a form, on a site where the add-on is holding a real house's grid
|
|
exchange at zero.
|
|
|
|
### 1.6 Reason three: it would promote 27 unpinned literals to load-bearing
|
|
|
|
And the cost compounds with §2 of this document. `test_defaults_pin.py` part 4
|
|
ledgers **27 (module, key) pairs** in `app/` that still answer a missing option
|
|
with an in-code literal. Today those literals are unreachable on a Supervisor
|
|
install, because the deep-merge guarantees every `options:` key is present in
|
|
the served blob. Remove keys from `options:` to hide them and those literals
|
|
stop being dead code: they become **the actual source of the defaults on a live
|
|
install** — the CFG-03 defect the same card's AC 4 exists to police, made
|
|
load-bearing by the card's own AC 3.
|
|
|
|
### 1.7 What now grades this
|
|
|
|
`tools/supervisor_shape_probe.py` §F. The count was printed there from the day
|
|
it was written — *"the Configuration tab renders 71 fields"* — and the probe
|
|
exited 0 through UX-01's whole review while displaying the number that
|
|
contradicted the criterion it was evidence for. **A measurement that is
|
|
displayed but not asserted is how this passed review.** Three assertions
|
|
replace the bare print:
|
|
|
|
```
|
|
PASS every one of the 71 `schema:` keys is ALSO in `options:`, so schema-only
|
|
- the only set HA's toggle can hide - is EMPTY. Hideable: []
|
|
PASS ...so replaying the frontend's own _filteredSchema on the served options
|
|
hides NOTHING: 71 of 71 fields render
|
|
PASS ...and with `options:` EMPTIED ENTIRELY - the maximum hiding that is
|
|
expressible - the tab still renders 59 required fields. THAT is the
|
|
floor, and AC 3 asked for ~15.
|
|
```
|
|
|
|
Every number in them is computed from the manifest and from Supervisor's own
|
|
`required` flag; none is typed. **The red direction was run**: deleting one key
|
|
(`price_feed_url`) from `options:` and nothing else turns the first two to FAIL
|
|
— `Hideable: ['price_feed_url']`, `70 of 71 fields render` — and the probe
|
|
exits 1. The counterfactual is isolated: one key varied, nothing else.
|
|
|
|
So if a future editor empties `options:` to shorten the form, this goes red and
|
|
somebody has to read §1.5 before merging.
|
|
|
|
---
|
|
|
|
## 2. The defaults pin: `test_defaults_pin.py`
|
|
|
|
### 2.1 What AC 4 asked for and what shipped
|
|
|
|
AC 4 has three clauses.
|
|
|
|
| clause | state before UX-04 |
|
|
|---|---|
|
|
| "every `schema:` key absent from `options:` has exactly one defaults-table entry" | **vacuous** — that set is empty (§1.2), so the clause is true of any tree, including one with no defaults at all |
|
|
| "the table changed → red" | **works**, six times over, once per mirror in its own suite |
|
|
| "no module carries its own literal fallback for a schema default — an AST check, not a grep" | **never built** |
|
|
|
|
The only literal scan in the repository before this card is
|
|
`test_validate.py:1025`: a `re.findall` restricted to eight `maintenance_*`
|
|
keys across three named files. It is a grep, it predates UX-01, and it was not
|
|
written for this.
|
|
|
|
### 2.2 What an "in-code literal fallback" is, structurally
|
|
|
|
A call in `app/` taking a string literal that names a `schema:` key, with a
|
|
constant in the next positional slot:
|
|
|
|
```python
|
|
opts.get("max_w", 2000) _number(opts, "max_w", 2000.0)
|
|
```
|
|
|
|
Found by walking `ast.Call` nodes — so a multi-line call and a renamed `opts`
|
|
are found, and the same text inside a comment or docstring is not. Two
|
|
exclusions, both structural and both argued at the site:
|
|
|
|
* **`getattr(obj, "max_w", 0.0)`** reads an attribute off an object, not an
|
|
option off a blob. Excluded by callee name.
|
|
* **the empty string.** `opts.get("static_profile", "")` is Python's "not
|
|
stated", and all six such sites in `validate.py` normalise it immediately —
|
|
`str(opts.get(k, "") or "").strip()`. It cannot be a stale copy of a *number*,
|
|
which is the whole of the CFG-04 defect class. Excluded by value. **`0` is
|
|
not excluded**: `slew_w: 0` and `integrator_max_w: 0` are real shipped
|
|
defaults with real meanings.
|
|
|
|
### 2.3 What it grades, measured on `release/1.0` @ `6e7b233`
|
|
|
|
```
|
|
2. the mirrors, DISCOVERED from the tree rather than listed here
|
|
6 table(s): circulation.py:CIRC_OPTION_DEFAULTS, delivery.py:DELIVERY_OPTION_DEFAULTS,
|
|
maintenance.py:MAINT_OPTION_DEFAULTS, peakguard.py:PEAK_OPTION_DEFAULTS,
|
|
price.py:PRICE_OPTION_DEFAULTS, watchdog.py:SIGN_OPTION_DEFAULTS
|
|
30 mirrored key(s) of 71 in `schema:`
|
|
3. the AST scan
|
|
scanned 29 modules, 34 literal fallback site(s): 0 over mirrored keys,
|
|
34 over unmirrored ones
|
|
4. the CFG-03 ledger
|
|
27 (module, key) pair(s) carry a literal for a key no mirror answers for
|
|
```
|
|
|
|
Four properties, none of them vacuous:
|
|
|
|
1. **The mirrors are discovered, not listed.** Six suites already pin their own
|
|
table to `config.yaml` — and every one of those pins names its table by
|
|
hand, so a *seventh* table added tomorrow would be pinned by nobody and
|
|
nothing would say so. This walks `app/` for module-level `*_OPTION_DEFAULTS`
|
|
assignments and grades whatever it finds against `config.yaml`. That is the
|
|
property the six hand-written pins cannot have.
|
|
2. **No literal survives for a key a mirror already answers for.** 30 keys,
|
|
0 violations.
|
|
3. **The CFG-03 ledger is FROZEN, by (module, key) pair, not by count.** A
|
|
count goes green again the moment somebody deletes one site and adds
|
|
another. Line numbers are deliberately *not* in the key: a ledger that goes
|
|
red on an unrelated reformat is a ledger people learn to re-bless without
|
|
reading. The 27 pairs are the open CFG-03 backlog; they are **not** removed
|
|
here, because that is behaviour change across `main.py`, `p1.py`,
|
|
`inverter.py` and `validate.py` that no single card should make in passing.
|
|
The set can now only shrink.
|
|
4. **Every remaining literal is graded against `config.yaml`** — the half that
|
|
catches CFG-04's actual failure mode, two copies of one number and one moves.
|
|
|
|
### 2.4 One divergence found, and it is new
|
|
|
|
```
|
|
main.py target_grid_w: code 0 vs config.yaml -10
|
|
```
|
|
|
|
`main.py:290` reads `target_grid_w=float(opts.get("target_grid_w", 0))` while
|
|
`config.yaml` ships **-10**. A dict handed to `Controller` without the key holds
|
|
net exchange at 0 W instead of the shipped -10 W bias into import. **It cannot
|
|
reach an operator**: on a Supervisor install the deep-merge guarantees the key
|
|
is present, and the only paths that hand over a partial dict are
|
|
`sim/inject.py` and this repository's own fixtures. It is recorded in the check
|
|
as the single permitted divergence, with the reason, rather than corrected —
|
|
changing it alters the control loop's target and belongs to a card that grades
|
|
the loop. A *second* divergence goes red.
|
|
|
|
### 2.5 ⭐ The red direction, shown rather than claimed
|
|
|
|
A check that has never been shown failing is not a check — which is exactly
|
|
what clause one of this AC was. Two things follow from that:
|
|
|
|
**(a) The red direction runs on every invocation.** Part 5 feeds the scanner a
|
|
synthetic module containing a deliberate violation of each rule and *requires*
|
|
it to be flagged, in-process, through the same `literal_fallbacks()` the graded
|
|
parts call. A scanner that goes blind — a renamed helper, an `ast` change, a
|
|
`SCHEMA` that comes back empty — stops working here too, and the total site
|
|
count is additionally graded against a floor.
|
|
|
|
**(b) A literal was planted in the real tree and the check went red.** Planted
|
|
at `main.py:290`, `float(opts.get("maintenance_charge_w", 2000))` — a mirrored
|
|
key:
|
|
|
|
```
|
|
3. the AST scan: no module carries its own literal fallback for a MIRRORED schema default
|
|
scanned 29 modules, 35 literal fallback site(s): 1 over mirrored keys, 34 over unmirrored ones
|
|
ok the scanner is not blind: it found 35 sites (floor 20)
|
|
main.py:290 get('maintenance_charge_w', 2000)
|
|
FAIL no literal fallback survives for any of the 30 keys a mirror already
|
|
answers for - those sites must index the mirror
|
|
|
|
1 FAILED (exit 1)
|
|
```
|
|
|
|
And part 4's two directions, planted the same way with
|
|
`opts.get("p1_host", "x")` and `opts.get("slew_w", 999)`:
|
|
|
|
```
|
|
4. the CFG-03 ledger
|
|
28 (module, key) pair(s) carry a literal for a key no mirror answers for
|
|
FAIL no NEW module/key pair has started carrying a literal fallback [('main.py', 'p1_host')]
|
|
main.py p1_host: code 'x' vs config.yaml ''
|
|
main.py slew_w: code 999 vs config.yaml 0
|
|
main.py target_grid_w: code 0 vs config.yaml -10
|
|
FAIL every remaining literal still AGREES with config.yaml, except the 1
|
|
divergence(s) recorded above it
|
|
|
|
(exit 1)
|
|
```
|
|
|
|
The second of those is the CFG-04 defect class itself: a literal that has
|
|
drifted away from `config.yaml` while the file still says something else. Both
|
|
plants were removed; the tree is byte-identical to `6e7b233` apart from this
|
|
card's own files.
|
|
|
|
---
|
|
|
|
## 3. CFG-04: closed on `release/1.0`, by UX-02, not by UX-01
|
|
|
|
UX-01's AC 5 claimed the double-literal `max_w` hole. It did not close it.
|
|
`fef6ef2` touched `config.yaml`, `translations/`, `test_translations.py` and the
|
|
probe — **no defaults code at all** — and on the 0.7.0 tree the triple mutation
|
|
survives: 21 files, 0 RED.
|
|
|
|
**Confirmed on current `release/1.0` @ `6e7b233`, 2026-09-07.** The same triple
|
|
mutation, `2000 -> 2500` at the three descendants of 0.7.0's
|
|
`inverter.py:438` / `validate.py:383` / `validate.py:684`:
|
|
|
|
| 0.7.0 | `release/1.0` @ `6e7b233` |
|
|
|---|---|
|
|
| `inverter.py:438` `max_w=float(opts.get("max_w", 2000))` | `inverter.py:640` `raw = opts.get("max_w", 2000)` |
|
|
| `validate.py:383` `cap = _number(opts, "max_w", 2000.0)` | `validate.py:466`, same line |
|
|
| `validate.py:684` `max_w = _number(opts, "max_w", 2000.0)` | `validate.py:836`, same line |
|
|
|
|
Result — 26 files, **RED at `test_selfconfig.py`**, exit 1:
|
|
|
|
```
|
|
1 FAILED: ⚠️ CLEARING max_w ON THE LIVE SHAPE IS REFUSED - rule 10 catches the
|
|
rail collapsing to the 2000 default under an explicit 400 W slew, so the box
|
|
cannot be emptied into a quietly halved rail
|
|
```
|
|
|
|
`git log -L 310,325:goodwe_controller/test_selfconfig.py` attributes that check
|
|
to **`bcee235`, "UX-02: pin both halves of the max_w fix, because neither was
|
|
graded", glenn schrooyen, 2026-09-06** — not to UX-01. The hole ships closed
|
|
with the pending 0.9.0 deploy. **The AC is met by UX-02.** No work was re-done
|
|
here; the mutation was run once, on a throwaway copy, to confirm it.
|
|
|
|
---
|
|
|
|
## 4. The 8 per-entry translations: they stay
|
|
|
|
### 4.1 The measurement, and it is negative
|
|
|
|
`translations/{en,nl}.yaml` carry a name and a description for all 8 per-entry
|
|
keys of the repeatable `inverters:` section. They are loaded by Supervisor's own
|
|
loader, they pass `SCHEMA_APP_TRANSLATIONS`, and `test_translations.py` grades
|
|
their coverage. **They do not reach the screen**, and this is not a defect in
|
|
them. `UiOptions` gives `inverters` `type: "schema"` with `multiple: True`, and
|
|
the frontend's `_convertSchemaElement` sends exactly that case to a selector
|
|
rather than to a nested `ha-form`:
|
|
|
|
```js
|
|
if ("schema" === t.type && !t.multiple) return {...type:"expandable"...} // not us
|
|
if ("schema" === t.type) { const o = {}; for (const a of t.schema)
|
|
o[a.name] = {required: a.required, selector: ...}; return {object:{multiple:t.multiple, fields:o}} }
|
|
```
|
|
|
|
and `ha-selector-object` labels those fields `fields[name].label || name`, while
|
|
`_convertSchemaElementToSelector` sets **neither `label` nor `translation_key`**.
|
|
So an operator sees raw `name`, `setpoint_entity`, `max_w`, `bms_soc_floor`. The
|
|
probe already asserts this, negatively and by name.
|
|
|
|
### 4.2 Decision: keep them. Four reasons.
|
|
|
|
1. **The Supervisor tab cannot be made to render them.** The gap is in
|
|
somebody else's converter, not in this repository. There is no change here
|
|
that fixes it; deleting the translations does not fix it either.
|
|
2. **[[UX-02]]'s ingress panel is where they become visible**, and it renders
|
|
the roster itself rather than handing it to `ha-selector-object`. The
|
|
`fields:` block is the shape Supervisor's own schema defines for exactly
|
|
this, and it is what a surface that wires `label` through consumes.
|
|
3. **They cost nothing.** No runtime path reads them; they are two YAML blocks
|
|
in files the Supervisor already parses, and the parse is graded green.
|
|
4. **Deleting them would remove the only thing that catches a per-entry key
|
|
added without a translation.** `test_translations.py` grades top-level and
|
|
per-entry coverage symmetrically; drop the per-entry half and SAFETY-32's
|
|
`bms_soc_floor` — a *required* per-entry key added after UX-01 — would have
|
|
joined the roster with no label and nothing to say so.
|
|
|
|
This is a written decision, not a change. The tree is unmodified in
|
|
`translations/`.
|
|
|
|
### 4.3 Not re-filed
|
|
|
|
`tools/supervisor_selfapi_probe.py` has been known red since OPT-09 — it
|
|
hardcodes `len(_sub) == 7` against 8 per-entry keys. That is **TOOL-04**,
|
|
already filed, and it is not re-filed or fixed here. It is named only so a
|
|
reader of §4.1 knows why the *other* probe is the one quoted.
|
|
|
|
---
|
|
|
|
## 5. What this card did not do
|
|
|
|
* It did not remove a key from `options:`. See §1.5 — the deep-merge default is
|
|
what makes the pending 0.9.0 upgrade safe on a 0.7.0 box.
|
|
* It did not pay down the 27-pair CFG-03 ledger. See §2.3.
|
|
* It did not correct `main.py`'s `target_grid_w` fallback. See §2.4.
|
|
* It did not touch `translations/`. See §4.2.
|
|
* It did not re-do CFG-04. See §3.
|
|
* It did not touch hardware, the live Home Assistant at 192.168.2.158, or
|
|
`sim/`. Both probe runs were throwaway containers over a read-only mount.
|