UX-13 P3: Simple view and Batteries screen
- Batteries: one card per battery with On/Off, the mode list (cheap hours and selling greyed out until OPT-11 P3), level, and Now / Today / Why lines built in homeview.py. Replaces the OPT-11 P1 minimal cards and uses the existing pack_use / pack_mode actions and store keys. - An Off battery the peak limit still needs says so with its figure; no line shows 0 W beside moving power (OPT-11 P1 test F2). Today says "Not planned yet" until OPT-11 P3/P4. - Home's plan leaves Off batteries out, except for the peak limit. - Settings opens on Simple: the layout's simple fields with an applies chip and a note, plus the battery switches and modes. Installer is one tap. - English and Dutch words in app/ui_text; test_batteries_screen.py; browser check tools/ux13_browser_check.py (CHECK=1 ux13_screenshots.sh); new and changed screenshots only. No control, allocator, arbiter or command module changed. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01UBtqPrG1mDLXzTEBnhEkkV
This commit is contained in:
@@ -32,6 +32,10 @@
|
||||
let FILLED = {}; // field key -> the preset field that fills it
|
||||
let view = null; // "settings" or "setup": the tab drawing V
|
||||
let STEPS = {}; // wizard step -> refusals that block it
|
||||
// P3: "simple" (the default, for home owners) or "installer". Remembered in
|
||||
// this browser only; a blocked storage just means Simple each time.
|
||||
let mode = "simple";
|
||||
try { if (localStorage.getItem("ux13.settings.view") === "installer") mode = "installer"; } catch (e) { /* no storage */ }
|
||||
let uid = 0;
|
||||
const ROWS = [];
|
||||
const OPEN = new Set();
|
||||
@@ -337,7 +341,7 @@
|
||||
function fieldRow(f, card, gid) {
|
||||
if (f.control === "inverter_list") return cardList(f, gid);
|
||||
const lid = "sf" + (++uid);
|
||||
const keys = keyText(f, card);
|
||||
const keys = gid === "simple" ? "" : keyText(f, card); // Simple shows no keys
|
||||
const msg = h("div", { class: "msgs" });
|
||||
const make = CONTROLS[f.control] || text;
|
||||
const row = h("div", { class: "f" + (f.control === "toggle" ? " inline" : ""), "data-key": f.key, "data-card": card },
|
||||
@@ -421,10 +425,79 @@
|
||||
d.append(body);
|
||||
host.append(d);
|
||||
}
|
||||
renderSimple();
|
||||
paint();
|
||||
window.scrollTo(0, y);
|
||||
}
|
||||
|
||||
/* ---------------- Settings, Simple (P3) ---------------- */
|
||||
// The fields the layout marks `simple`, each in its own card with when it
|
||||
// applies, and the batteries' switches and modes (runtime state, applied at
|
||||
// once) drawn by app.js from /status. The Installer rows stay built but
|
||||
// hidden, so the save bar counts every unsaved change in either view.
|
||||
const chip = (applies) => h("span", { class: "chip " + (applies === "now" ? "now" : "rs") }, ROOT.applies[applies].name);
|
||||
// A note with {kw} is filled from /status (app.js sends ux13:status).
|
||||
function fillNotes() {
|
||||
const st = window.UX13.status();
|
||||
const mp = st && st.month_peak;
|
||||
document.querySelectorAll("#simple [data-kw-note]").forEach((e) => {
|
||||
const ok = mp && mp.peak_w != null;
|
||||
e.hidden = !ok;
|
||||
e.textContent = ok ? e.dataset.kwNote.replace("{kw}", window.UX13.kw(mp.peak_w) + " kW") : "";
|
||||
});
|
||||
}
|
||||
document.addEventListener("ux13:status", fillNotes);
|
||||
function simpleField(f) {
|
||||
const row = fieldRow(f, null, "simple");
|
||||
row.querySelector(".head").append(chip(f.applies));
|
||||
const n = f.note && W.simple.note[f.note];
|
||||
if (n && n.description.includes("{kw}")) {
|
||||
row.append(h("div", { class: "small muted", "data-kw-note": n.description, hidden: true }));
|
||||
} else if (n) {
|
||||
// A plain note replaces the option's installer help on this view.
|
||||
const help = row.querySelector(".help");
|
||||
if (help) help.textContent = n.description;
|
||||
}
|
||||
return h("div", { class: "card" }, row);
|
||||
}
|
||||
function renderSimple() {
|
||||
const host = $("#simple");
|
||||
host.replaceChildren();
|
||||
if (mode !== "simple") return;
|
||||
const cards = allFields().filter((f) => f.simple).map(simpleField);
|
||||
const batts = h("div", {});
|
||||
cards.splice(Math.min(1, cards.length), 0, h("div", { class: "card" },
|
||||
h("div", { class: "row spread" }, h("div", { class: "lbl" }, W.simple.batteries.name), chip("now")), batts));
|
||||
host.append(...cards, h("p", { class: "small muted", style: "margin:4px 2px" }, W.simple.more.description + " ",
|
||||
h("button", { type: "button", class: "btn link", onclick: () => setMode("installer", true) }, W.simple.installer)));
|
||||
window.UX13.batteries(batts);
|
||||
fillNotes();
|
||||
}
|
||||
function setMode(v, redraw) {
|
||||
mode = v;
|
||||
try { localStorage.setItem("ux13.settings.view", v); } catch (e) { /* no storage */ }
|
||||
document.querySelectorAll("#set-view [role=radio]").forEach((b) => {
|
||||
const on = b.dataset.v === v;
|
||||
b.setAttribute("aria-checked", String(on)); b.tabIndex = on ? 0 : -1;
|
||||
});
|
||||
$("#set-desc").textContent = v === "simple" ? W.simple.description : W.description;
|
||||
$("#simple").hidden = v !== "simple";
|
||||
["#set-search", "#groups", "#set-nomatch"].forEach((id) => { $(id).classList.toggle("vhide", v === "simple"); });
|
||||
if (redraw && M && view === "settings") render();
|
||||
}
|
||||
document.querySelectorAll("#set-view [role=radio]").forEach((b, i, all) => {
|
||||
b.addEventListener("click", () => setMode(b.dataset.v, true));
|
||||
b.addEventListener("keydown", (e) => {
|
||||
const d = { ArrowRight: 1, ArrowDown: 1, ArrowLeft: -1, ArrowUp: -1 }[e.key];
|
||||
if (!d) return;
|
||||
e.preventDefault();
|
||||
const j = all[(i + d + all.length) % all.length];
|
||||
setMode(j.dataset.v, true); j.focus();
|
||||
});
|
||||
});
|
||||
setMode(mode, false);
|
||||
window.UX13.settingsView = (v) => setMode(v, true);
|
||||
|
||||
/* ---------------- messages, visibility, search, badges ---------------- */
|
||||
const ICON = { bad: "×", warn: "!", info: "i" };
|
||||
const RANK = { bad: 0, warn: 1, info: 2 };
|
||||
@@ -434,11 +507,13 @@
|
||||
return m.card == null ? r.card === 0 : m.card === r.card;
|
||||
}
|
||||
function paint() {
|
||||
const q = view === "setup" ? "" : $("#set-search").value.trim().toLowerCase();
|
||||
const q = view === "setup" || mode === "simple" ? "" : $("#set-search").value.trim().toLowerCase();
|
||||
// A message counts as shown only beside a row of the view on screen.
|
||||
const onScreen = (r) => view === "setup" || (mode === "simple") === (r.gid === "simple");
|
||||
const placed = new Set();
|
||||
for (const r of ROWS) {
|
||||
r.visible = visible(r.f);
|
||||
const msgs = MSG.filter((m, i) => { const hit = mine(r, m); if (hit && r.visible) placed.add(i); return hit; })
|
||||
const msgs = MSG.filter((m, i) => { const hit = mine(r, m); if (hit && r.visible && onScreen(r)) placed.add(i); return hit; })
|
||||
.sort((a, z) => RANK[a.tier] - RANK[z.tier]);
|
||||
r.tiers = new Set(msgs.map((m) => m.tier));
|
||||
r.msg.replaceChildren(...msgs.map((m) => h("div", { class: "msg " + m.tier },
|
||||
@@ -562,7 +637,9 @@
|
||||
validate();
|
||||
}
|
||||
function firstBad() {
|
||||
const r = ROWS.find((x) => x.tiers && x.tiers.has("bad") && x.visible);
|
||||
if (view === "settings" && mode === "simple" && !ROWS.some((x) => x.gid === "simple" && x.tiers && x.tiers.has("bad") && x.visible)
|
||||
&& ROWS.some((x) => x.tiers && x.tiers.has("bad") && x.visible)) setMode("installer", true);
|
||||
const r = ROWS.find((x) => x.tiers && x.tiers.has("bad") && x.visible && (view !== "settings" || (mode === "simple") === (x.gid === "simple")));
|
||||
if (!r) { if (!$("#set-other").hidden) $("#set-other").scrollIntoView({ block: "center" }); return; }
|
||||
for (let p = r.el.parentElement; p; p = p.parentElement) if (p.tagName === "DETAILS") p.open = true;
|
||||
r.el.scrollIntoView({ block: "center" });
|
||||
|
||||
Reference in New Issue
Block a user