UX-10: the last plain-language leftovers: log lines, two table rows, 'Why: Other', red at 0 s
checks / unit (pull_request) Successful in 9m3s
infra-pin / fresh (pull_request) Successful in 2s

- test_messages.py part 6: every dynamic part of a WARNING+ line is classified
  as data or as a built text, and every built text is produced by the real
  code and checked. This is the gap the 2d scan had: it counted `%s` as one
  word, so reasons built by loop_clamp_note() and friends were never seen.
- Quoted reasons rewritten to STYLE.md (red-alarm shape, codes kept):
  OUTPUT_CLAMP, METER_FROZEN, CIRCULATING_POWER, STALE_READING, the peak
  guard (codes now PEAK_GUARD*), the migration report, price feed, price
  history and gate reasons. Plurals via messages.times()/count().
- Top table: Price history "96 prices over 24 hours for 26 Sep", Tariff
  "one every 15 minutes or every hour", "not final yet" dropped.
- Levels and noise: per-cycle shadow line DEBUG, shadow on/off INFO,
  aiohttp access log only at log level debug, settings warnings once per
  start, one plain line per Home Assistant outage (hass.Outage).
- INFO jargon and error texts: no "§8.3", ISO times, "banked", "keys ARE",
  "NOT_OBSERVED", "did NOT land", "AND".
- Display bugs: WHY_WINNER covers every winner and safety reason
  (main.why_key(), part 7 finds them in the code); the meter row is amber
  "waiting for the first reading" inside its limit after a start; the
  missing-reading age is counted when the page asks.
- STYLE.md and DECISIONS/UX-08-short-notifications.md (UX-10 section).

No change to safety behaviour, alarm levels, notification ids, triggers,
cadence or the banner cascade. One row colour changes (meter at start),
justified in DECISIONS.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
glenn schrooyen
2026-09-27 10:58:43 +02:00
co-authored by Claude Opus 5.5
parent 125b0702a2
commit 23a3a3f5b5
33 changed files with 1268 additions and 347 deletions
+19 -6
View File
@@ -4107,7 +4107,8 @@ _T11_SLOTS = {
"carried": r"\d+ published and (\d+) estimated",
"horizon": r"for (\d+) hours ahead",
"age_s": r"Price plan from (\d+) s ago",
"spacings": r"([\d+]+) s apart",
# UX-10: the spacing is in words ("one every 15 minutes or every hour").
"spacings": r"one (every [a-z0-9 ]+?)(?:,|\.|$)",
# ⚠️ THE *Arbitrage* ROW'S OWN AGE SLOT, and it is deliberately the same
# pattern for "decided N s ago" and "the last one was taken N s ago" -
# WHICH of the two sentences was rendered is graded by inequality below,
@@ -4125,6 +4126,18 @@ _T11_SLOTS = {
}
def _spacing_s(raw):
"""`every 15 minutes or every hour` -> `[900, 3600]`; raises otherwise."""
unit = {"second": 1, "minute": 60, "hour": 3600}
out = []
for part in raw.split(" or "):
m = re.fullmatch(r"every (?:(\d+) )?(second|minute|hour)s?", part.strip())
if m is None:
raise ValueError(f"no spacing in {part!r}")
out.append(int(m.group(1) or 1) * unit[m.group(2)])
return out
def _slot(row, name):
"""The number in ONE NAMED SLOT of a rendered row, or `_Raised`.
@@ -4140,18 +4153,18 @@ def _slot(row, name):
if m is None:
raise ValueError(f"no {name!r} slot in {row[:90]!r}")
raw = m.group(1)
return ([int(x) for x in raw.split("+")] if name == "spacings"
else int(raw))
return (_spacing_s(raw) if name == "spacings" else int(raw))
return derived(_get)
_T11_GOOD = ("x. Price plan from 7 s ago, for 48 hours ahead: day-ahead "
"prices from the price feed, 132 prices over 33 hours, "
"36 published and 96 estimated, 900+3600 s apart")
"36 published and 96 estimated, one every 15 minutes or every "
"hour")
_T11_SHIPPED_DEFECT = ("x. Price plan from 7 s ago, for 48 hours ahead: "
"day-ahead prices from the price feed, 132 prices over "
"132 hours, 96 published and 36 estimated, 900+3600 s "
"apart")
"132 hours, 96 published and 36 estimated, one every "
"15 minutes or every hour")
# The slots divide into the ones a *Tariff*/​*Arbitrage* ROW carries and the
# ones a REFUSAL sentence carries; each group is calibrated against its own
# well-formed example, because a slot asked of the wrong kind of sentence is