Phase 3.5 — Dashboard rewrite (multi-plan ranked view)#81
Conversation
Adds an OptionsFlow step that pins ONE CDR plan from the current ranked alternatives as a "named comparator", and wires the coordinator to construct a second CdrPlanProvider for that plan under the literal `"named"` key in `_providers`. The named provider then participates in the existing 30s tick loop (no new tick path) and contributes a `"named"` column to `daily_cost_history` at daily rollover, which the Phase 3.4 commit 2 rollup sensors will read. - `const.py`: add CONF_NAMED_COMPARATOR_PLAN_ID + CONF_NAMED_COMPARATOR_PLAN. We persist the FULL PlanDetailV2 body (not the summarised form) because the evaluator needs tariffPeriod data the summary omits. - `config_flow.py`: new `named_comparator` menu entry + step. The step reads ranked_alternatives + the per-day _ranking_plan_cache directly from the coordinator. Aborts with `no_ranked_alternatives` if either is empty (covers the post-install + post-midnight-cache-reset edge cases per plan §4.2 #1 + #3). Decision tree extracted to `plan_named_comparator_step()` so it's unit-testable without HA's app context (the OptionsFlow class itself becomes a MagicMock under the conftest mock tree). - `coordinator.py`: new `build_named_comparator_provider()` module-level helper (same testability rationale as build_backfill_plan_set). Called from both __init__ AND rebuild_engine so a fresh pin lands on the next OptionsFlowWithReload cycle without an HA restart. - `strings.json` + `translations/en.json`: new step title/description + menu label + two abort reasons. Keep distinct from the existing `comparators` step (which toggles live-API providers, not CDR pins). - 22 new tests: 10 in test_config_flow_phase_3 (full decision-tree coverage including the full-body persistence guard, dedupe, default fallback when prior pin evicted from cache, plan_not_in_cache branch), 4 in test_coordinator_helpers (lifecycle of the named provider helper across all option shapes). Lock interaction with ranking lock: none. The named comparator joins the existing tick loop unchanged. The OptionsFlow step is a READ from _ranking_plan_cache, which is only written under the ranking lock — reading without the lock is safe because (a) the worst case is a brief torn read that resolves to the abort path, and (b) the lock is held for the duration of the ranking pipeline run, not just dict mutation. 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Surfaces the Phase 3.4 commit 1/2 wiring as user-visible HA sensors: - `sensor.pricehawk_named_cost_today` - `sensor.pricehawk_named_cost_week` - `sensor.pricehawk_named_cost_month` - `sensor.pricehawk_named_cost_3month` - `sensor.pricehawk_named_cost_year` Subclasses the Phase 3.3 `PeriodRollupSensor` base; overrides `native_value` + `extra_state_attributes` to read the `"named"` key from `daily_cost_history` rather than extend the base's `_ROLLUP_KIND` dispatch enum (which Phase 3.3 just shipped — localise the new behaviour here instead of rewriting the base contract for one extra kind). Registration is CONDITIONAL on `"named" in coordinator._providers`, so users who haven't pinned a plan don't see 5 permanently- unavailable entities clutter their HA UI. Reads `_providers` directly (not `data["providers"]`) so the registration check fires on first setup before the coordinator has populated its data dict. CHANGELOG.md gains a Phase 3.4 block under [Unreleased] documenting the OptionsFlow step, the new sensors, lock/ranking interaction (none), and the persistence-through-rank-churn behaviour (the pin survives the plan dropping out of cheap-rank top-K because it lives in options, not in derived ranking state). strings.json + translations/en.json gain entity blocks for the 5 new sensors. Descriptions call out the tick-by-tick cadence difference vs the other ranked alternatives (which only refresh at daily rollover) so users know what they bought by pinning. 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Two CodeRabbit findings on the Phase 3.4 named comparator.
1. The named provider added to `self._providers["named"]` was
constructed in `__init__` and `_apply_options_update` but never
persisted or restored. Every other CdrPlanProvider in the
coordinator (current plan, amber, flow_power, localvolts) round-
trips through `_async_persist_state` / `async_restore_state`, so
on HA restart their per-day accumulators (import_cost_today, kwh)
survive — but `named` would reset to zero while the others kept
state, so today's rollup deltas would lie until midnight rollover.
Added named.to_dict() to the persist block and a from_dict() call
in restore, using `today = dt_util.now().date()` (already in scope
above) to satisfy the AEGIS rule that from_dict MUST receive an
explicit HA-timezone date (no `date.today()` fallback).
2. `GenericProviderCostSensor(provider_id="named")` produces
unique_id `{entry}_named_cost_today`. `NamedComparatorRollupSensor`
for the "today" window produces the same unique_id (since its
_ROLLUP_KIND is "named" and the base class composes
`{kind}_cost_{window}`). HA's entity registry drops the second
registration silently, breaking whichever sensor the dashboard
depends on first. Added a `provider_id == "named"` skip in the
providers loop in `async_setup_entry` — the named comparator is
exposed via its dedicated 5-window rollup family instead. Also
skips the matching GenericProviderRateSensor pair so we don't
register orphan rate sensors for the "named" key. (Only one such
loop exists; the Phase 3.3 rollup loop and Phase 3.4 named-rollup
loop don't have the same collision since their _ROLLUP_KIND
differs.)
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
…multi-plan layout Full rewrite of custom_components/pricehawk/www/dashboard.html (2447 -> 940 LOC) replacing the Amber-vs-current-plan two-comparator view with the multi-plan ranked layout from plan section 5.1. Visual language ported from assets/dashboard-v3-apple.html (dark default, Outfit + IBM Plex Mono, noise + ambient bg). Per-provider colour tokens (--amber-primary, --globird-primary) replaced with semantic ones (--accent-positive, --accent-negative, --accent-neutral) per the Phase 3.0 pivot away from provider-specific branding. Scaffold layout per plan section 5.1: - NAV bar (brand + connection status + clock + theme toggle) - HERO row: current cost card + savings-vs-best-alt card - PERIOD TABS: [Today][Week][Month*][3 Month][Year], active swaps data - RANKED ALTERNATIVES table: #/plan/peak/supply/saving, click -> drill - DRILL-IN CARD: per-plan stats + "Pin as Named Comparator" button - DATA HEALTH FOOTER: backfill state / days loaded / last ranking / count Entity reads are NOT wired yet — sample data renders so the scaffold is visually verifiable before commit 3.5/2 binds real sensor values. WebSocket connection logic copied verbatim from the previous dashboard: - WS URL derived from location.protocol (AEGIS rule: never hardcode ws://) - Token from URL params, postMessage, parent.hassConnection, or localStorage hassTokens (AEGIS rule: never hardcode the token) CSP connect-src extended to include ws(s)://*.local:* so the dashboard works on Ryan's HA Green at homeassistant.local (plan section 5.3 surprise #1). Existing localhost + Nabu Casa entries preserved. Active period tab persists to localStorage so re-opens land on the user's last view rather than defaulting to month every time. 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
…ll entity reads
Hooks the scaffold from commit 3.5/1 up to the Phase 3.2 / 3.3 / 3.4
sensors. After WebSocket auth completes, fires a get_states + subscribes
to state_changed events for the 16 tracked entities.
Tracked entities (per plan section 5.2 + Phase 3.3 / 3.4 worker notes):
- 5 x sensor.pricehawk_current_cost_{today,week,month,3month,year}
- 5 x sensor.pricehawk_best_alt_cost_{...} (NOT _best_alternative_cost_)
- 5 x sensor.pricehawk_savings_{...}
- 5 x sensor.pricehawk_named_cost_{...} (NOT _named_comparator_cost_)
- sensor.pricehawk_ranked_alternatives
- sensor.pricehawk_backfill_status
Hero row binding:
- Current cost card reads sensor.pricehawk_current_cost_<window>.
- Savings card reads sensor.pricehawk_savings_<window> and colours green
/ red / muted around the +/- $0.005 deadband.
- Best-alt name pulled from ranked_alternatives.attributes.alternatives[0]
(sensor is sorted ascending by cheap-rank score per summarize_for_sensor).
- Projected annual extrapolates active-window savings * 365/window_days.
Period tabs swap activeWindow and re-call renderHero() — all rollup
bindings re-evaluate against the new window's entity ID. Active class
mirrors localStorage so the tab UI stays in sync on cold loads.
Ranked alts table render:
- Pulls ranked_alternatives.attributes.alternatives[].
- Renders rank-pill (#1 gold), plan name + brand, peak rate, supply,
saving. Saving column only fills for the #1 plan (the cheapest); #2..N
show "—" because we don't have per-alt cost rollups — only the best-alt
rollup. Avoids fabricating numbers that don't match the sensor.
- Click row → drill-in card slides up below + plan ID persists in
selectedPlanId so re-renders after state_changed events preserve
selection.
- Empty state ("Waiting for the daily ranking job…") covers first-install
before the first ranking run completes.
Drill-in card:
- Stats grid: peak rate, daily supply, customer type, plan ID,
cheap-rank score (when present).
- "Pin as Named Comparator" deep-links to the integration's Configure
page (/config/integrations/integration/pricehawk). Per plan section 5.3
surprise #2 + plan section 9 REVISIT 4: HA doesn't support per-step
deep-linking; the deep-link is the locked UX for this phase.
Data Health footer renders backfill state with state-coloured value
(green=complete / amber=running / red=failed / muted=idle), days_loaded,
ranked_alternatives.last_run as relative + absolute time, and the
alternatives count.
Empty-state UI for first-run users (plan section 5.3 surprise #3): when
backfill_status.days_loaded < 7, hero rollup values are replaced with an
"Accruing… [n/365]" pill instead of showing $0.00 — surfaces clearly
that we don't have enough history yet rather than implying zero spend.
XSS hardening: all attribute-sourced strings (plan_id, display_name,
brand, customer_type) pass through escapeHtml() before innerHTML
insertion. Catches any future CDR registry payloads that include
HTML-ish characters in brand names.
30s setInterval re-renders the ranked + footer cards so the relative
timestamps ("ran 27s ago / 3h ago") tick forward without waiting for
the next state_changed event.
TDZ fix: the period-tab boot block previously called setActiveWindow()
before the entity state store consts were declared, which tripped a
ReferenceError on attrs in strict mode. Boot now defers the first full
render to the explicit boot block at the script bottom.
🤖 Generated with [Claude Code](https://claude.com/claude-code)
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
…GELOG entry Wraps Phase 3.5 with the two non-code deliverables called out in plan section 5.2 commit 3.5/3. assets/DESIGN.claude.md: - New "PriceHawk Dashboard (divergence from this spec)" section at the end of the file. Explains WHY PriceHawk doesn't follow the Claude marketing-site spec (different surface context, different information density, different brand) and WHAT it does inherit (typographic rationale, card-as-surface model, accent-discipline rule). - Documents the PriceHawk token map (--bg-base, --accent-positive etc) for cross-reference. - Keeps the rest of the Claude marketing-site spec intact — no edits outside the new appended section. CHANGELOG.md: - Phase 3.5 block at the top of [Unreleased] above the existing 3.4 entry. Documents the dashboard rewrite (entity bindings, period-tab swap, ranked alts render, drill-in, footer, empty-state), the CSP connect-src extension for *.local deployments, the deleted per-provider colour tokens, the deleted Amber-specific cards, and the manual-UAT-only test strategy (per plan section 6.3 table). dashboard_config.py: NO behavioural change. Plan section 5.2 commit 3.5/3 calls for a "verify cache-busting still works" check; verified that `?v=<version>.<epoch>` is appended in setup_panel_iframe and is independent of dashboard.html contents — the rewrite doesn't affect it. No source edit needed. 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
CodeRabbit / markdownlint MD040: the fenced code block listing the PriceHawk CSS custom properties (--bg-base, --bg-surface, --accent-positive et al) opened with a bare ``` instead of ```css. Tag the fence as ``css`` so the markdown renderer applies CSS syntax highlighting and so MD040 stops flagging the block. Content is unchanged. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
There was a problem hiding this comment.
Sorry @Artic0din, you have reached your weekly rate limit of 1500000 diff characters.
Please try again later or upgrade to continue using Sourcery
|
Important Review skippedAuto reviews are disabled on base/target branches other than the default branch. 🗂️ Base branches to auto review (3)
Please check the settings in the CodeRabbit UI or the ⚙️ Run configurationConfiguration used: Path: .coderabbit.yaml Review profile: ASSERTIVE Plan: Pro Run ID: You can disable this status message by setting the Use the checkbox below for a quick retry:
✨ Finishing Touches🧪 Generate unit tests (beta)
Comment |
Summary
Phase 3.5 dashboard rewrite. Originally PR #76, auto-closed when stacked base was deleted on #73 merge. Stacked on new Phase 3.3+3.4 PR.
Changes
custom_components/pricehawk/www/dashboard.html(~1257 LOC, down from 2447)Test plan
🤖 Generated with Claude Code