UX-13 P2: the Installer settings view
- Settings tab: the Installer view (app/static/settings.js), drawn from
app/uilayout.py through the new app/uipage.py: search, six collapsible
groups with "n to fix / n to check / n changed" badges and an "applies
after a restart" chip, radio cards, segmented choices, sliders with units
(the peak limit in kW), entity pickers with search and the live reading,
inverter cards, solar rows, locked measured values behind "Change", and
the add-on's refusals, warnings and info beside the field they name.
- Save path: the page posts its model to the existing config/validate and
config/save; uimodel turns it into options before the shipped validator,
so nothing it refuses is saved. No route added. A value equal to the
stored one is written as stored, so a save that changed nothing writes an
identical blob. Save bar, then the restart dialog that names 0 W.
- The region choice shows its preset year; Wallonia and Brussels are listed
as having no preset yet (TARIFF-22). "Classic settings" keeps /config.
- P1 test findings: (a) "Going to 0 W" until the last accepted power request
is 0 W; (b) the trade blocks and the price line name the feed's own points
("Lowest published price"), the hour bars say "hour average"; (c) the entity
list's WARNING gives a reason on a timeout.
- test_settings.py; test_home, test_haentities, test_uilayout extended.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01LyR88rXETvwhTgx6yHE39p
This commit is contained in:
@@ -40,10 +40,12 @@
|
||||
clearTimeout(toast.timer); toast.timer = setTimeout(() => e.classList.remove("show"), 2600);
|
||||
}
|
||||
let onYes = null;
|
||||
// `key` names Home's words in ui_text, or is {name, description, yes}.
|
||||
function ask(key, fn) {
|
||||
$("#dlg-t").textContent = t("dialog." + key + ".name");
|
||||
$("#dlg-b").textContent = t("dialog." + key + ".description");
|
||||
$("#dlg-yes").textContent = t("dialog." + key + ".yes");
|
||||
const w = typeof key === "string" ? t("dialog." + key) : key;
|
||||
$("#dlg-t").textContent = w.name;
|
||||
$("#dlg-b").textContent = w.description;
|
||||
$("#dlg-yes").textContent = w.yes;
|
||||
onYes = fn;
|
||||
const d = $("#dlg");
|
||||
if (typeof d.showModal === "function") d.showModal(); else if (window.confirm($("#dlg-t").textContent)) fn();
|
||||
@@ -98,7 +100,6 @@
|
||||
renderControl(structured);
|
||||
renderDetails(!structured);
|
||||
renderBatteries();
|
||||
renderSettings();
|
||||
}
|
||||
|
||||
/* ---------------- Home: now ---------------- */
|
||||
@@ -162,7 +163,7 @@
|
||||
const title = b.label + " " + t("plan.time_to") + " " + b.end_label;
|
||||
if (b.eur_kwh == null) return chartItem("i", "none", "", title, [t("plan.none_tip") + "."]);
|
||||
const kind = b.kind === "cheap" ? ", " + t("plan.cheap_tip") : b.kind === "dear" ? ", " + t("plan.dear_tip") : "";
|
||||
const est = b.estimate ? ", " + t("plan.estimate_tip") : "";
|
||||
const est = (b.points > 1 ? ", " + t("plan.average_tip") : "") + (b.estimate ? ", " + t("plan.estimate_tip") : "");
|
||||
return chartItem("i", (b.kind || "mid") + (b.estimate ? " est" : ""),
|
||||
"height:" + Math.max(4, Math.round(Math.abs(b.eur_kwh) / mx * 100)) + "%",
|
||||
title, [eur(b.eur_kwh) + " " + t("plan.per_kwh") + kind + est + "."]);
|
||||
@@ -289,7 +290,7 @@
|
||||
if (open) $("#c-details").open = true;
|
||||
}
|
||||
|
||||
/* ---------------- Batteries and Settings (placeholders until P2/P3) ---------------- */
|
||||
/* ---------------- Batteries (placeholder until P3) ---------------- */
|
||||
function renderBatteries() {
|
||||
if (!S.batteries) { $("#bcards").replaceChildren(); return; }
|
||||
$("#bcards").replaceChildren(...S.batteries.map((b) => h("div", { class: "card" },
|
||||
@@ -301,10 +302,6 @@
|
||||
h("dt", {}, t("batteries.now")), h("dd", {}, b.now),
|
||||
h("dt", {}, t("batteries.mode")), h("dd", {}, b.mode)))));
|
||||
}
|
||||
function renderSettings() {
|
||||
const line = S.plan && S.plan.region_line;
|
||||
$("#set-region").textContent = line || ""; $("#set-region").hidden = !line;
|
||||
}
|
||||
|
||||
/* ---------------- control buttons ---------------- */
|
||||
// The action is a toggle, so the page reads the live state first and sends
|
||||
@@ -334,6 +331,9 @@
|
||||
$("#btn-maint").addEventListener("click", () => ask("maint_start", () => simple("maint_start")));
|
||||
$("#btn-abort").addEventListener("click", () => ask("maint_abort", () => simple("maint_abort")));
|
||||
|
||||
// Shared with settings.js, which loads after this file.
|
||||
window.UX13 = { h, ask, toast, t, maint: (what) => ask(what, () => simple(what)) };
|
||||
|
||||
/* ---------------- tabs: hash routes, arrow keys ---------------- */
|
||||
const TABS = [...document.querySelectorAll(".tab")];
|
||||
function select(tab, focus) {
|
||||
@@ -345,6 +345,7 @@
|
||||
if (focus) tab.focus();
|
||||
if (location.hash !== "#" + tab.dataset.hash) history.replaceState(null, "", "#" + tab.dataset.hash);
|
||||
schedule();
|
||||
document.dispatchEvent(new CustomEvent("ux13:tab", { detail: tab.dataset.hash }));
|
||||
}
|
||||
TABS.forEach((x, i) => {
|
||||
x.addEventListener("click", () => select(x, false));
|
||||
|
||||
Reference in New Issue
Block a user