UX-08 2d: plain log lines, the rest of the status page, and DOCS
checks / unit (pull_request) Successful in 8m55s
infra-pin / fresh (pull_request) Successful in 1s

Every WARNING and ERROR line is one plain line of at most 25 words, with
no card ids, capitals or semicolons; the reasoning of the ten longest is
on a DEBUG line. The ux08 allowlist is empty and its cap is 0 (43 -> 0).

The status page: the month-peak and peak-guard rows, the stale-reading
row, the top table (Why, Tariff, Arbitrage, Shadow mode, labels), the
Stopped banner and the line under the buttons, which now shows the plain
hint of each event instead of the raw action log. The Supervisor question
at start logs one line with a real reason.

Refusals are counted as shown (labels expanded, long cases driven), seven
were shortened, the card and parse errors lost their internals, a blank
value reads "empty", and the panel no longer files inverters.max_w under
the flat max_w. Option names lose "(JSON)", the NL texts are checked too.

DOCS gets a glossary (control board, isolator, Developer Tools) and card
ids move from headings to a "For installers" line.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01LyR88rXETvwhTgx6yHE39p
This commit is contained in:
2026-09-27 08:01:16 +02:00
co-authored by Claude Opus 5.5
parent a02cd4b713
commit 6a95447248
38 changed files with 1294 additions and 742 deletions
+34 -29
View File
@@ -204,6 +204,7 @@ import math
from dataclasses import dataclass
from datetime import datetime, timedelta, timezone
from . import messages
from .price import (
MAX_PLAUSIBLE_EUR_KWH, Absence, PricePoint, PriceProduct, PriceRead,
Uncertainty, _Outcome, brussels, load_prices, price_option,
@@ -597,11 +598,9 @@ def parse_static_profile(value, *, option: str = "static_profile",
value = json.loads(text)
except ValueError as e:
raise ProfileInvalid(
f"{option} is not valid JSON ({e}). Write it on one line with "
f"a \"periods\" list, for example\n"
f' {{"periods": [{{"name":"day","from":"07:00","to":"22:00",'
f'"days":"mon-fri","eur_per_kwh":0.34}}, ...]}}\n'
f"Leave it empty if you have no fixed tariff.")
f"{messages.not_valid(option, e)}. Copy the example from the "
f"documentation, or leave it empty if you have no fixed "
f"tariff.")
if isinstance(value, dict):
tz = str(value.get("timezone", "") or "").strip().lower()
@@ -1120,18 +1119,24 @@ class TariffPlan:
for 76 % of a 40-hour soak and mixed 900/3600 s for 70 %, and no HTTP
surface said either.
"""
# UX-08: plain words, every figure in its own slot as before.
if not self.has_prices:
return f"no tariff - {self.read.reason}"
tag = {"provider": self.read.provider, "static": "static_profile",
"fallback": "static_profile (feed down)"}.get(self.origin,
self.origin)
return f"no prices, {self.read.reason}"
tag = {"provider": "the price feed", "static": "your fixed tariff",
"fallback": "your fixed tariff, because the feed is down"}.get(
self.origin, self.origin)
what = {PriceProduct.DAY_AHEAD: "day-ahead prices",
PriceProduct.REALTIME: "real-time prices",
PriceProduct.STATIC: "day/night prices",
PriceProduct.FIXED: "a fixed price"}.get(self.product,
str(self.product))
gaps = self.spacings_s
return (f"{self.product.value} from {tag}, "
f"{self.n_points} points over {self.hours} h, "
f"{self.n_firm} firm / {self.n_carried} carried"
+ (f", {'+'.join(str(g) for g in gaps)} s spacing"
return (f"{what} from {tag}, "
f"{self.n_points} prices over {self.hours} hours, "
f"{self.n_firm} published and {self.n_carried} estimated"
+ (f", {'+'.join(str(g) for g in gaps)} s apart"
if gaps else "")
+ (", provisional" if self.provisional else ""))
+ (", not final yet" if self.provisional else ""))
def _whole_hours(points) -> int:
@@ -1225,21 +1230,22 @@ def static_status(opts: dict, now: datetime):
plan = static_plan(opts, now, now + timedelta(days=MEAN_WINDOW_DAYS),
now=now)
except ProfileInvalid as e:
_LOG.warning("static_profile is unusable (%s); no cheap window. This "
"should have been refused at startup - see validate.py", e)
return False, f"static_profile unusable: {e}"
_LOG.warning("static_profile cannot be used (%s), so there is no "
"cheap window. The start-up check should have refused "
"it.", e)
return False, f"Fixed tariff cannot be used: {e}"
if not plan.has_prices:
return False, "no static profile"
return False, "No fixed tariff set"
cheap = plan.cheap_now(now)
window = plan.window_at(now)
price = plan.price_at(now)
when = plan.profile.next_change(now)
until = "" if when is None else f" until {brussels(when):%H:%M}"
kind = ("fixed" if plan.product is PriceProduct.FIXED else "day/night")
return cheap, (f"{kind}: {window}{until} at {price:.4f} EUR/kWh, "
f"mean {plan.mean_eur_kwh():.4f} over "
f"{MEAN_WINDOW_DAYS} d - "
f"{'CHEAP window' if cheap else 'not a cheap window'}")
kind = ("Fixed" if plan.product is PriceProduct.FIXED else "Day/night")
return cheap, (f"{kind} tariff, {window}{until}: {price:.4f} EUR/kWh, "
f"{'cheap' if cheap else 'not cheap'} against the "
f"{MEAN_WINDOW_DAYS}-day average of "
f"{plan.mean_eur_kwh():.4f}")
async def resolve_tariff(opts: dict, since: datetime, until: datetime,
@@ -1336,13 +1342,12 @@ def _degrade(opts, read, since, until, stamp) -> TariffPlan:
"(%s), so there is no tariff at all", e)
profile = None
if profile is None:
_LOG.warning("§11.4: %s, and no static_profile is configured to fall "
"back to. The plan is provisional and arbitrage is off; "
"self-consumption, peak shaving and reserve management "
"continue (§6, §10.1).", read.reason)
_LOG.warning("%s, and no static_profile is set to fall back to, so "
"prices are provisional and trading is off. Peak "
"limiting and home use go on.", read.reason)
return TariffPlan(read, None, "none")
_LOG.warning("§11.4: %s - falling back to the static profile, plan marked "
"PROVISIONAL, arbitrage off.", read.reason)
_LOG.warning("%s, so the static_profile tariff is used instead, marked "
"provisional, and trading is off.", read.reason)
points = profile.series(since, until, provisional=True)
return TariffPlan(
PriceRead.series(points, profile.product, "static_profile", stamp),