UX-13 P2 review fixes (PR #145, comment 1880)
1. Discard reloads /config/model: after a stale save the page kept the
new fingerprint with the old values, so Discard + Save overwrote the
other surface's change without a 409.
2. Removing cards back to one restores the stored inverter shape, so a
flat site stays flat after add + remove.
3. A choice the page can only keep ("Other time windows") stays offered
while it is the stored value.
4. A load locks every locked value again.
5. at_zero() needs an accepted 0 W write; before the first one Home says
"Going to 0 W" (the heartbeat writes every heartbeat_s, so it clears).
6. A field with a unit drops the translation's "(W)" from its label: the
peak limit read "Peak limit (W)" beside a kW value.
7. A mirrored choice lists the mirrored field's keys, so a search for
static_profile finds the contract on a fixed-price site.
8. test_settings: a save refused by ui_to_options writes nothing.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
@@ -511,10 +511,12 @@ def build_plan(c, now, shadow, running, held=False, settled=True):
|
||||
# Batteries, attention, month peak, control
|
||||
# --------------------------------------------------------------------------- #
|
||||
def at_zero(inv) -> bool:
|
||||
"""Has this inverter's last accepted power request reached 0 W? A stop or
|
||||
an alarm asks for 0 W at once, but the inverter gets it with the next
|
||||
write, so until then the page says "Going to 0 W" (UX-13 P1 test, a)."""
|
||||
return abs(inv.standing_w()) < 1.0
|
||||
"""Has this inverter accepted a 0 W power request? A stop or an alarm asks
|
||||
for 0 W at once, but the inverter gets it with the next write, so until
|
||||
then the page says "Going to 0 W" (UX-13 P1 test, a). Before the first
|
||||
accepted write nothing is known to be held; the heartbeat writes every
|
||||
`heartbeat_s`, stopped or not, so this does not stick."""
|
||||
return inv.last_write is not None and abs(inv.last_write) < 1.0
|
||||
|
||||
|
||||
def battery_rows(c, shadow, running, held=False):
|
||||
|
||||
@@ -87,8 +87,10 @@
|
||||
}
|
||||
|
||||
/* ---------------- controls ---------------- */
|
||||
function choicesOf(f, cur) {
|
||||
const out = (f.choices || []).filter((c) => !c[2] || same(c[0], cur));
|
||||
// A choice the page can only keep stays offered while it is the stored value.
|
||||
function choicesOf(f, card) {
|
||||
const cur = get(f, card), was = valueIn(ORIG, f, card);
|
||||
const out = (f.choices || []).filter((c) => !c[2] || same(c[0], cur) || same(c[0], was));
|
||||
if (cur != null && cur !== "" && !out.some((c) => same(c[0], cur))) out.push([cur, String(cur)]);
|
||||
return out;
|
||||
}
|
||||
@@ -106,7 +108,7 @@
|
||||
function radios(f, card, lid, cards) {
|
||||
const cur = get(f, card);
|
||||
const g = h("div", { class: cards ? "choices" : "seg", role: "radiogroup", "aria-labelledby": lid });
|
||||
const items = choicesOf(f, cur);
|
||||
const items = choicesOf(f, card);
|
||||
const buttons = items.map(([v, label], i) => {
|
||||
const on = same(v, cur);
|
||||
const hint = cards && f.hints ? f.hints[v] : "";
|
||||
@@ -132,7 +134,7 @@
|
||||
function select(f, card, lid) {
|
||||
const cur = get(f, card);
|
||||
const s = h("select", { class: "sel", "aria-labelledby": lid });
|
||||
for (const [v, label] of choicesOf(f, cur)) s.append(h("option", { value: JSON.stringify(v), selected: same(v, cur) }, label));
|
||||
for (const [v, label] of choicesOf(f, card)) s.append(h("option", { value: JSON.stringify(v), selected: same(v, cur) }, label));
|
||||
for (const [v, label] of f.later || []) s.append(h("option", { value: JSON.stringify(v), disabled: true }, label));
|
||||
s.addEventListener("change", () => put(f, card, JSON.parse(s.value), true));
|
||||
const preset = f.presets && f.presets[cur];
|
||||
@@ -324,7 +326,10 @@
|
||||
const box = h("div", { class: "inv" }, h("div", { class: "hd" },
|
||||
h("span", { class: "grow" }, c.name || W.inverter + " " + (i + 1)), h("span", { class: "key" }, f.key + "[" + i + "]"),
|
||||
cards.length > 1 ? h("button", { type: "button", class: "btn link tiny", onclick: () => {
|
||||
V[f.key].splice(i, 1); V[f.count] = V[f.key].length; put(f, null, V[f.key], true);
|
||||
V[f.key].splice(i, 1); V[f.count] = V[f.key].length;
|
||||
// Back to one card, the stored shape returns: a flat inverter stays flat.
|
||||
if (V[f.key].length === 1) for (const k in f.grow) V[k] = clone(ORIG[k]);
|
||||
put(f, null, V[f.key], true);
|
||||
} }, W.remove) : null));
|
||||
for (const s of f.fields) box.append(fieldRow(s, i, gid));
|
||||
wrap.append(box);
|
||||
@@ -496,6 +501,7 @@
|
||||
return;
|
||||
}
|
||||
M = m; V = clone(m.ui); ORIG = clone(m.ui); FP = m.fingerprint; MSG = []; DEVICE = "";
|
||||
UNLOCKED.clear();
|
||||
$("#set-search").hidden = false;
|
||||
FILLED = {};
|
||||
LIST = null;
|
||||
@@ -541,9 +547,9 @@
|
||||
finally { btn.disabled = false; btn.textContent = W.savebar.save; }
|
||||
}
|
||||
$("#sb-save").addEventListener("click", save);
|
||||
$("#sb-discard").addEventListener("click", () => {
|
||||
V = clone(ORIG); UNLOCKED.clear(); render(); validate(); toast(W.toast.discarded);
|
||||
if ($("#set-banner .b-bad")) banner();
|
||||
// Discard reloads what is stored: after a stale save ORIG is no longer it.
|
||||
$("#sb-discard").addEventListener("click", async () => {
|
||||
await load(); toast(W.toast.discarded);
|
||||
});
|
||||
addEventListener("beforeunload", (e) => { if (V && dirtyCount()) { e.preventDefault(); e.returnValue = ""; } });
|
||||
document.addEventListener("ux13:tab", (e) => {
|
||||
|
||||
@@ -91,6 +91,10 @@ def _choices(fld, node, T, value_type):
|
||||
def _field(fld, schema_ui, translations, lang, T):
|
||||
node = _node(schema_ui, fld.key)
|
||||
label, text = _words(translations, lang, fld.key, T)
|
||||
if fld.unit:
|
||||
# The page shows the unit beside the control, in its own scale (kW),
|
||||
# so the translation's "(W)" goes; "(optional)" stays.
|
||||
label = re.sub(r"\s*\([^()\s]{1,6}\)", "", label)
|
||||
value_type = ("float" if fld.control == L.NUMBER and fld.key.startswith("ui.")
|
||||
else node.get("type") or "")
|
||||
out = {"key": fld.key, "control": fld.control, "label": label,
|
||||
@@ -98,8 +102,9 @@ def _field(fld, schema_ui, translations, lang, T):
|
||||
"step": fld.step, "kind": fld.kind,
|
||||
"show": [list(c) for c in fld.show], "applies": fld.applies,
|
||||
"type": value_type, "optional": not node.get("required", False),
|
||||
# A choice mirrored into a field also shows (and finds) its keys.
|
||||
"writes": sorted(k for k, how in L.DERIVED.items()
|
||||
if how == fld.key)}
|
||||
if how in (fld.key, fld.mirror))}
|
||||
if node.get("lengthMin") is not None:
|
||||
out["min"] = node["lengthMin"]
|
||||
if node.get("lengthMax") is not None:
|
||||
|
||||
Reference in New Issue
Block a user