Prepares the split of app/main.py's Controller into files by job, with no app
change. tools/main_files.py gives a test the text and the AST of main.py plus
the files split from it, and `live`, which sets a global in every file whose
functions read it (and raises when none does). Tests that read main.py's
source, and tests and replay tools that patch a main global (datetime,
compute, resolve, _mono, WRITE_BUDGET_S, ...), go through it, so they pass on
this layout and on the split one. test_split_patches.py fails when a test or
tool patches an app.main attribute that main.py no longer reads, or that
another file also reads. tools/maint21_same_ast.py proves every function,
method and module-level assignment of main.py at the base is found unchanged
in its new file.
Co-Authored-By: Claude <noreply@anthropic.com>
- test_price_secret.py: the fixed-tariff note on the real /config/validate
route, EN and NL, and none for a spaces-only profile (web.py lang wiring).
- test_price_service.py: the start line is INFO, not WARNING; a dismiss that
raises does not stop the cycle.
- test_maint13/test_setup_only/test_watchdog: ignore only the dismiss of the
old card, so a create with that id is still counted.
- DECISIONS rule 7 quotes the settings note as it is; messages.py comment.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01Bdqr7PiaHcBnzPituu2yTC
G1: remove the false 'Fixed tariff not used' notification. One INFO line at
start and an info note beside the setting (EN/NL) when price_service has a
non-blank static_profile; the old notification id is dismissed at start.
DOCS, migration step 5 and DECISIONS rule 7 now say what the code does.
S05: whitespace-only static_profile counts as not set.
G2: maintenance_enabled read through MaintConfig.from_options().
Co-Authored-By: Claude <noreply@anthropic.com>
Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01Bdqr7PiaHcBnzPituu2yTC
- A price-provider site with no fixed tariff is not a gate (its plan never
sets `cheap`); DECISIONS §3 says so and a test pins it.
- The fixed-tariff check is worked out once an hour, not every tick.
- test_maint13: run() steps really are `step` minutes; the long loops use
5-minute steps (the per-tick cap); wall backstop checked "not before".
- test_safety93: the drain bound is the rig's configured drain rate.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
test_static_tariff forbids main.py reading `static_profile`; a small
static_gated() in static_tariff.py answers it, and the Controller keeps the
answer in `_price_gated` beside `cheap`.
Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01Bdqr7PiaHcBnzPituu2yTC
- gate_open is true with no price gate set up, in a cheap window, or while the
pack is taking charge; the stall rule and timeout now work on a no-tariff site.
- reset the save timer on a phase change; exact 5 min gap-cap test.
- test_safety93: original timeout scenario kept (B finished) plus a case where a
due B takes the slot after A's failure.
Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01Bdqr7PiaHcBnzPituu2yTC
A charge is complete at SoC >= target, or at >= target - 3 once the SoC has
made no new high for 60 min of allowed charging, or when the charging time
runs out at >= target - 3. The 36 h timeout counts only the minutes the price
gate let the pack charge (plus a 7 day wall-clock backstop). A cycle that
fails (stuck drain, charge below target - 3) backs off 7 days, tells the
operator once (EN + NL), and no cycle starts on that pack in that time unless
the operator forces one. State survives a restart; old state files load.
No option added. DECISIONS/MAINT-13-full-pack-is-complete.md.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>