UX-13 P0: layout table, page<->options derivation, warning tier, entity list
checks / unit (pull_request) Successful in 9m32s
infra-pin / fresh (pull_request) Successful in 1s

The foundation for the new settings page. No visible UI change.

- app/uilayout.py: screen -> group -> part -> field for every option in
  config.yaml (67 fields, 11 derived, 0 hidden), with control, unit, display
  scale, step, entity kind, show conditions, Simple/wizard placement and
  "applies after a restart". Ranges stay in the schema, words in
  translations and the new app/ui_text/en.json.
- app/uimodel.py: options_to_ui()/ui_to_options() for the derived keys (one
  inverter vs the list, solar rows, static_profile from simple prices, a
  year-stamped region preset for the levies figure, and an OPT-11 hook for
  arbitrage_enabled that changes nothing yet). Unchanged derivations write
  the stored values back, so the round trip is exact.
- validate.py: validate.Note, a warn/info tier. Each logged warning is also
  a note when a list is passed; config_notes() adds checks that need live
  data (missing entity, a kW sensor read as W, a limit below this month's
  peak). /config/validate returns them as "notes". No refusal changed: 261
  recorded validator calls, 0 differ (tools/ux13_validator_calls.py).
- GET ha/entities: Home Assistant's entity list for the picker, read through
  hass.states(), cached 30 s, 10 s timeout. When HA is down it answers 200
  with the old list marked old, or an empty list that says why.
- DECISIONS/UX-13-one-layout-table.md amends UX-02's "no option key in UI
  code": keys may appear in uilayout.py and uimodel.py only, both tested.
- Tests: test_uilayout, test_uimodel (round trip over the recorded calls,
  the TEST-23 option files and config.yaml defaults), test_haentities, a
  warning-tier part in test_validate; test_messages scans notes and
  ui_text; test_pv and test_selfconfig part 7 widened on purpose.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01LyR88rXETvwhTgx6yHE39p
This commit is contained in:
glenn schrooyen
2026-09-27 23:11:31 +02:00
co-authored by Claude Opus 5.5
parent 652097abde
commit 15627c38a6
24 changed files with 2224 additions and 23 deletions
@@ -0,0 +1,69 @@
# UX-13 — one layout table, and why UI code may now name an option key
**Status: DECIDED 2026-09-27 (owner approved the UX-13 design and mockup).**
Card 6ab979852f3eca02fb40f69e. Amends [[UX-02]] §3.1. Design note and
mockup: `goodwe-session/UX-13-design.md`, `UX-13-mockup.html`.
## 1. What UX-02 said, and why it no longer fits
UX-02 built the configuration panel from the Supervisor's schema, so
`app/configui.py` contains "no option key, no label and no rule", and
test_selfconfig.py part 5 checks that the page script names no key. That kept
a second copy of the option surface from drifting, and it is why the panel is
a flat list of fields in schema order.
UX-13 needs things a schema cannot say: groups and sub-groups, a slider in kW
for a value stored in W, a picker for an entity, a field shown only for one
meter source, locked measured values, a Simple view and a setup wizard. It
also needs page fields that are not options at all: one inverter card that is
stored as six flat keys, solar rows that become `pv_entity` or `pv_entities`,
a contract and two prices that become `static_profile` JSON, and a region
that becomes the levies figure. None of that can be read off the schema, so
some code has to name option keys.
## 2. The decision
Option keys may appear in UI code in exactly two files:
- **`app/uilayout.py`**, the layout table: screen → group → part → field, with
control, unit, display scale, step, entity kind, when it is shown, whether it
is on the Simple view or a wizard step, and whether it applies now or after a
restart. Every option in `config.yaml` is a field, `DERIVED` or `HIDDEN`,
exactly once.
- **`app/uimodel.py`**, the derivations the table names: `options_to_ui()` and
`ui_to_options()`. It may name only the keys it writes.
What UX-02 made authoritative stays authoritative:
- **Ranges** come from the schema (`uilayout.range_of()`); the table writes
none. A unit holds no digit.
- **Labels and help** come from `translations/*.yaml` by key. Page-only fields,
group titles and choices are in `app/ui_text/*.json`, held to STYLE by
test_messages.py.
- **Rules** stay in `validate.py`. It gains a warning tier (`validate.Note`,
"warn" or "info") for the page. A note never blocks a save, and passing the
notes list does not change the refusals returned.
- **The stored blob** is still the one truth. The page model carries the stored
values of every derived key (`keep`). A derivation whose page fields are
unchanged writes those values back untouched, so a blob the page cannot
express survives a save of another field.
## 3. How it is held
- test_uilayout.py fails when `config.yaml` gains an option, or an inverter
field, that the table does not place. It also checks that uimodel.py names
only the keys it writes and that web.py and haentities.py name none.
- test_uimodel.py round-trips options → page model → JSON → options for every
validator call the suite made at 652097a (`scenarios/ux13/`), the TEST-23
option files and config.yaml's defaults. All must be identical.
- `tools/ux13_validator_calls.py compare` runs those calls through the old and
new validator. At P0: 261 calls, 158 refused, no refusal changed.
- test_selfconfig.py part 5 still checks the old page script; P1 moves the page
into `app/static/` and the same check applies there.
## 4. What this costs
The table is a second place that knows the option surface, which UX-02 set
out to avoid. The cost is bounded by the coverage test: an option added to
config.yaml and not placed here fails CI, rather than silently missing from
the page. Changing a key's type or range needs no change here.