fix(ui): reconcile partial-month cost maths + add period navigation#452
Merged
Conversation
The home dashboard billed the standing charge for the FULL calendar month even mid-month, while energy/import/export covered only elapsed days. That inflated the Hero "net bill so far", understated/flipped "Saved vs Fixed", and made the cost-breakdown standing bar ~15x too tall early in the month. Two modules shared the root cause (monthrange() day-count paired with elapsed-day Fox energy). Backend (cost-axis reconciliation): - monthly.py: prorate standing to the elapsed-day window via a new `n_days` arg on _compute_cost/_compute_cost_octopus/_best_cost (defaults to full month → completed past months unchanged). Month/year/day/week branches and get_monthly_insights now pass the correct elapsed-day count (clamped to today.day, robust to Fox zero-padding). - monthly.py: bill realised export at per-slot Outgoing Agile rates (matching pnl.py:_realised_export_pence) instead of a flat manual rate. - monthly.py: compute a fixed-tariff shadow + delta_vs_fixed on the SAME metered kWh + day-window as the realised cost (no Fox-vs-Octopus meter mixing). day/week now route through the Octopus per-slot engine too. - tariff_engine.py: prorate _get_usage_data total_days for the current month. - Expose fixed_shadow_* / delta_vs_fixed_* on the cost response + UI types. Frontend (shared period navigation): - New lib/period.ts signal + PeriodNavigator (‹ label › + day/week/month/year) drive the whole home page; "next" disabled at the current period. - Hero re-scopes to the selected period and uses the backend delta_vs_fixed (drops the client-side Fox-kWh savings recompute); adds a live-now strip. - CostBreakdownChart renders the selected period (removes the month-avg-rate trailing-7d approximation). - TariffComparisonWidget uses the backend fixed shadow for the fixed row. - EnergyChartWidget reads the shared signal; past-day shows daily totals. Tests: new tests/test_monthly_cost_proration.py (proration, past-month regression guard, per-slot export, fixed shadow, tariff-engine days). Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This was referenced Jun 2, 2026
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Why
A review of the home dashboard surfaced wrong financial maths: for the current (in-progress) month the standing charge was billed for the full calendar month (
monthrange()days) while energy/import/export covered only elapsed days. Effects:(remaining_days × standing)(~£11.50 on the 2nd of a month).Two secondary axis inconsistencies were folded in per the agreed scope: realised export billed at a flat rate (should be per-slot Outgoing Agile), and the savings calc mixing Fox-metered vs Octopus-metered kWh.
Separately, the user asked for day/week/month/year navigation across the dashboard.
Backend — cost-axis reconciliation (
src/energy/)n_daysarg on_compute_cost/_compute_cost_octopus/_best_cost(defaults to full month → completed past months byte-for-byte unchanged). Month/year/day/week branches +get_monthly_insightspass the elapsed-day count, clamped totoday.day(robust to Fox zero-padding).pnl.py:_realised_export_pencepattern viadb.get_agile_export_rates_in_range), not a flat manual rate.fixed_shadow_*,delta_vs_fixed_*) computed on the same metered kWh + day-window as realised cost — kills the Fox-vs-Octopus mix. day/week now route realised cost through the Octopus per-slot engine too (manual fallback preserved for un-backfilled days).tariff_engine._get_usage_dataproratestotal_daysfor the current month.src/api/models.py,src/api/main.py).Frontend — shared period navigator (
ui/)lib/period.tssignal +PeriodNavigator(‹ label ›+ day/week/month/year toggle) drives the whole page; "next" disabled at the current period.delta_vs_fixed_pounds(drops the client-side Fox-kWh savings recompute); adds a "live now" strip that ignores the selector.Tests
tests/test_monthly_cost_proration.py: proration, past-month regression guard, per-slot export billing, fixed-shadow correctness, tariff-engine day count.tsc --noEmit+npm run buildclean.test_dhw_policy,test_pv_calibration_3d,test_tariff_aware_load_profile) fail on the base branch too.Verification
standing_charge_pence / chart_data.length ≈ standing/day, past month bills full month unchanged, navigator steps across all granularities, standing bar no longer towers early in the month.No pre-existing tracking issue; touches the
#424per-slot-history area (navigator now allows past-day selection, flagged accordingly).🤖 Generated with Claude Code