Files
goodwe-addon/goodwe_controller/DECISIONS/UX-13-one-layout-table.md
T
adminandClaude Opus 5.5 be57b87c34
checks / unit (pull_request) Successful in 10m22s
infra-pin / fresh (pull_request) Successful in 1s
UX-13 P4: the setup wizard for installers
The Setup tab draws the Installer view's page model one step at a time,
from each field's `wizard` step in app/uilayout.py: meter (with the kettle
test), inverters and batteries, solar (optional), prices, peak limit
(battery modes: "comes in a later update", OPT-11), check and start.
It reuses P2's controls and saves through /config/validate and
/config/save. No route was added.

- First run: /config/model says `setup_needed` when the stored options name
  no power request entity, or on a new install (no `setup_done` and no
  `auto` in the Store, control not started). The page opens #setup only
  when opened with no route. The wizard's save writes `setup_done`.
- Kettle test: GET ha/entities?entity= reads one entity now, in W. The
  change decides: up keeps meter_invert off, down turns it on; a net or
  import/export meter that goes down blocks the step with what to check.
- Last step: validate.setup_checks() beside the refusals: the meter reads,
  each power request entity exists in HA (bad if not, SAFETY-77), battery
  levels and power with direction, solar sign, prices. Saves with
  shadow_mode on by default, then a confirmed restart naming the 0 W.
- P2 test finding (a): a stale save lists only the keys the other surface
  moved (the blob handed out on open, by fingerprint, against stored now).
- test_setup.py; tools/ux13_wizard_blobs.py makes the 88 blobs the wizard
  can save; all accepted by Supervisor 2026.08.0 AppOptions (offline).
- DECISIONS/UX-13-setup-wizard.md; DOCS "Setup, for a new install".

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01LyR88rXETvwhTgx6yHE39p
2026-09-28 05:31:18 +02:00

4.2 KiB

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.
  • P2: the Installer view (app/static/settings.js) draws from app/uipage.py, which serialises the table and names no key itself (test_uilayout part 5, test_settings part 2). The view posts its page model to the existing /config/validate and /config/save, which turn it into options with ui_to_options() before the shipped validator runs. No route was added.
  • P4: the setup wizard draws the same model by each field's wizard step. Its tables (KETTLE, SHADOW) are in uilayout.py, and its live checks are rules, so they are in validate.py (setup_checks). settings.js still names no key. See UX-13-setup-wizard.

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.