UX-13 P5: Dutch text, STYLE check, DOCS screenshots, period editor
checks / unit (pull_request) Successful in 10m45s
infra-pin / fresh (pull_request) Successful in 1s

- app/ui_text/nl.json and app/uitext.py: every page word in Dutch, English
  fallback per key; the page picks by browser language. Home's sentences
  move into ui_text `home_words`, so /status?lang=nl builds them in Dutch.
- test_ui_text.py: EN/NL key and placeholder parity, STYLE limits, jargon
  lists for both languages, no inline details, untranslated Dutch caught.
  Shared rules moved unchanged to tools/style_rules.py.
- tools/ux13_screenshots.sh/.py: DOCS screenshots from the demo fixtures in
  headless Chromium; docs/img/ux13/. Battery modes (P3): no screenshot.
- "Other time windows": a list editor (ui.periods) that builds
  static_profile and names bad times, empty prices, overlaps and gaps.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
Claude dev agent
2026-09-28 16:33:32 +02:00
parent 8c34647d05
commit 667b8e9145
38 changed files with 1633 additions and 300 deletions
+103
View File
@@ -217,6 +217,109 @@ check("the hook answers None while battery modes do not exist",
check("so the toggle is stored as the page has it",
edit(DEFAULTS, arbitrage_enabled=True)["arbitrage_enabled"] is True)
print("\n7. the time-window editor (UX-13 P5)")
def rows(*spec):
return [{"from": a, "to": b, "days": d, "price": p} for a, b, d, p in spec]
def periods(r, base=SITE):
return edit(base, ui__contract="custom", ui__fallback="custom",
ui__periods=r)
def refused(r):
"""(code, values) of the DeriveError the rows give, or None."""
try:
periods(r)
except U.DeriveError as err:
return err.code, err.values
return None
# Five windows the day/night preset cannot say: a weekday peak, a weekday
# night, Saturday split at noon, and Sunday.
_five = rows(("06:30", "21:15", "mon-fri", 0.31), ("21:15", "06:30", "mon-fri", 0.19),
("00:00", "12:00", "sat", 0.2), ("12:00", "00:00", "sat", 0.25),
("00:00", "24:00", "sun", 0.18))
_p5 = periods(_five)
_prof5 = parse_static_profile(_p5["static_profile"])
check("five odd windows write a profile the tariff parser reads, with every "
"price in place", [p.eur_per_kwh for p in _prof5.periods]
== [0.31, 0.19, 0.2, 0.25, 0.18] and _prof5.periods[3].end_min == 1440)
check("...that the shipped start checks accept, and that reads back as the "
"same rows", not validate_startup_config(_p5)
and U.options_to_ui(_p5)["ui.contract"] == "custom"
and [(r["from"], r["to"], r["days"], r["price"])
for r in U.options_to_ui(_p5)["ui.periods"]]
== [("06:30", "21:15", "mon-fri", 0.31), ("21:15", "06:30", "mon-fri", 0.19),
("00:00", "12:00", "sat", 0.2), ("12:00", "24:00", "sat", 0.25),
("00:00", "24:00", "sun", 0.18)])
_named = dict(SITE, static_profile=json.dumps({"periods": [
{"name": "peak", "from": "08:00", "to": "20:00", "days": "mon-sun",
"eur_per_kwh": 0.33},
{"name": "offpeak", "from": "20:00", "to": "08:00", "days": "mon-sun",
"eur_per_kwh": 0.21}]}))
_rows = U.options_to_ui(_named)["ui.periods"]
_rows[1]["price"] = 0.2
check("a stored profile's window names are kept when a price changes",
[p.name for p in parse_static_profile(periods(_rows, base=_named)
["static_profile"]).periods]
== ["peak", "offpeak"])
check("the day and night preset shows as three rows, so 'Other time windows' "
"starts from what is stored",
[r["days"] for r in U.options_to_ui(
edit(SITE, ui__contract="daynight", ui__day_price=0.3,
ui__night_price=0.2))["ui.periods"]]
== ["mon-fri", "mon-fri", "sat,sun"])
check("an overlap is named: the two windows, the day and the times",
refused(rows(("07:00", "23:00", "mon-fri", 0.3),
("22:00", "07:00", "mon-fri", 0.2),
("00:00", "24:00", "sat,sun", 0.2)))
== ("period_overlap", {"day": "mon", "from": "22:00", "to": "23:00",
"a": 1, "b": 2}))
check("a gap is named: the day and the times",
refused(rows(("07:00", "22:00", "mon-sun", 0.3),
("22:00", "24:00", "mon-sun", 0.2)))
== ("period_gap", {"day": "mon", "from": "00:00", "to": "07:00"}))
check("a gap on one day only is found on that day",
refused(rows(("00:00", "24:00", "mon-fri", 0.3),
("00:00", "24:00", "sat", 0.2)))
== ("period_gap", {"day": "sun", "from": "00:00", "to": "24:00"}))
check("bad times are named by window: '7h', '25:00', '07:60', '24:00' as a "
"start", all(refused(rows(("00:00", "24:00", "mon-sun", 0.2), (a, b, "mon", 0.1)))
== ("period_time", {"row": 2}) for a, b in
(("7h", "08:00"), ("25:00", "08:00"), ("07:60", "08:00"),
("24:00", "08:00"), ("07:00", "")))
and refused(rows(("7:00", "07:00", "mon-sun", 0.2)))
== ("period_zero", {"row": 1}))
check("an empty price, and a price in cents, are named by window",
refused(rows(("00:00", "24:00", "mon-sun", None)))
== ("period_price", {"row": 1})
and refused(rows(("00:00", "24:00", "mon-sun", 34)))
== ("period_price_range", {"row": 1}))
check("no windows at all is refused", refused([]) == ("no_periods", {}))
from app import uipage # noqa: E402
_msg = uipage.derive_message(U.DeriveError("x", "period_overlap", "ui.periods",
day="sat", a=1, b=3, **{
"from": "06:00", "to": "07:00"}))
_nl = uipage.derive_message(U.DeriveError("x", "period_gap", "ui.periods",
day="sun", **{"from": "00:00",
"to": "07:00"}), "nl")
check(f"the page shows the error beside the editor, with the day by name "
f"({_msg['text']!r}, {_nl['text']!r})",
_msg["field"] == "ui.periods" and "Windows 1 and 3" in _msg["text"]
and "Saturday 06:00 to 07:00" in _msg["text"]
and "zondag 00:00 tot 07:00" in _nl["text"])
_raw = json.dumps({"periods": [
{"from": "00:00", "to": "24:00", "days": "mon,wed", "eur_per_kwh": 0.2},
{"from": "00:00", "to": "24:00", "days": "tue,thu-sun", "eur_per_kwh": 0.3}]})
check("days the editor has no choice for stay in the Configuration tab, and "
"survive a save", U.options_to_ui(dict(SITE, static_profile=_raw))
["ui.periods"] is None and edit(dict(SITE, static_profile=_raw), gain=0.5)
["static_profile"] == _raw)
print()
if fails:
print(f"{len(fails)} FAILED")