Skip to content

fix(ui): reconcile partial-month cost maths + add period navigation#452

Merged
albinati merged 1 commit into
mainfrom
fix/ui-cost-math-and-period-nav
Jun 2, 2026
Merged

fix(ui): reconcile partial-month cost maths + add period navigation#452
albinati merged 1 commit into
mainfrom
fix/ui-cost-math-and-period-nav

Conversation

@albinati

@albinati albinati commented Jun 2, 2026

Copy link
Copy Markdown
Owner

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:

  • Hero "net bill so far" inflated by (remaining_days × standing) (~£11.50 on the 2nd of a month).
  • "Saved vs Fixed" understated / flipped (counterfactual used elapsed-day standing, realised used full-month).
  • CostBreakdown standing bar ~15× too tall early in the month (full-month standing ÷ elapsed days).
  • Same bug inflated the tariff-comparison table standing + the "last Nd of usage" label.

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/)

  • Standing proration: new n_days arg 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_insights pass the elapsed-day count, clamped to today.day (robust to Fox zero-padding).
  • Realised export now billed at per-slot Outgoing Agile rates (reuses the pnl.py:_realised_export_pence pattern via db.get_agile_export_rates_in_range), not a flat manual rate.
  • Fixed-tariff shadow (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_data prorates total_days for the current month.
  • New fields exposed on the cost response (src/api/models.py, src/api/main.py).

Frontend — shared period navigator (ui/)

  • New lib/period.ts signal + PeriodNavigator (‹ label › + day/week/month/year toggle) drives the whole page; "next" disabled at the current period.
  • Hero re-scopes to the selected period and uses the backend delta_vs_fixed_pounds (drops the client-side Fox-kWh savings recompute); adds a "live now" strip that ignores the selector.
  • CostBreakdownChart renders the selected period (removes the month-avg-rate trailing-7d approximation).
  • TariffComparisonWidget uses the backend fixed shadow for the fixed row (consistent with the realised current-tariff row).
  • EnergyChartWidget reads the shared signal; navigating to a past day shows daily totals (per-slot history is still UI energy chart: capture per-30min solar / grid import / export for day-view breakdown #424).

Tests

  • New tests/test_monthly_cost_proration.py: proration, past-month regression guard, per-slot export billing, fixed-shadow correctness, tariff-engine day count.
  • Backend: all monthly/period/pnl/tariff/insights tests pass. UI: tsc --noEmit + npm run build clean.
  • Pre-existing unrelated date-flakes (test_dhw_policy, test_pv_calibration_3d, test_tariff_aware_load_profile) fail on the base branch too.

Verification

  • ✅ Backend unit tests (proration + regression guard) green.
  • ✅ UI type-check + build green; API server boots, routes register.
  • ⏳ Full end-to-end (real Fox/Octopus data) verifies on prod after deploy: current-month 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 #424 per-slot-history area (navigator now allows past-day selection, flagged accordingly).

🤖 Generated with Claude Code

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>
@albinati albinati merged commit 055bbe2 into main Jun 2, 2026
4 of 5 checks passed
@albinati albinati deleted the fix/ui-cost-math-and-period-nav branch June 2, 2026 15:28
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant